├── .clang-format ├── .editorconfig ├── .github └── workflows │ └── windows-build.yml ├── .gitignore ├── .gitmodules ├── 3rdparty └── CMakeLists.txt ├── CMakeLists.txt ├── LICENSE ├── README.md ├── assets └── fonts │ ├── ForkAwesome │ ├── LICENSE.txt │ └── forkawesome-webfont.ttf │ └── Roboto │ ├── LICENSE.txt │ └── Roboto-Medium.ttf ├── images ├── sponza.jpg └── sponza_cluster_vis.png └── src ├── CMakeLists.txt ├── Cluster.cpp ├── Cluster.h ├── Config.cpp ├── Config.h ├── Log ├── AssimpSource.h ├── Log.cpp ├── Log.h └── UISink.h ├── Renderer ├── ClusterShader.cpp ├── ClusterShader.h ├── ClusteredRenderer.cpp ├── ClusteredRenderer.h ├── DeferredRenderer.cpp ├── DeferredRenderer.h ├── ForwardRenderer.cpp ├── ForwardRenderer.h ├── LightShader.cpp ├── LightShader.h ├── PBRShader.cpp ├── PBRShader.h ├── Renderer.cpp ├── Renderer.h ├── Samplers.h └── Shaders │ ├── clusters.sh │ ├── colormap.sh │ ├── cs_clustered_clusterbuilding.sc │ ├── cs_clustered_lightculling.sc │ ├── cs_clustered_reset_counter.sc │ ├── cs_multiple_scattering_lut.sc │ ├── fs_clustered.sc │ ├── fs_clustered_debug_vis.sc │ ├── fs_deferred_fullscreen.sc │ ├── fs_deferred_geometry.sc │ ├── fs_deferred_pointlight.sc │ ├── fs_forward.sc │ ├── fs_tonemap.sc │ ├── lights.sh │ ├── pbr.sh │ ├── samplers.sh │ ├── tonemapping.sh │ ├── util.sh │ ├── varying.def.sc │ ├── vs_clustered.sc │ ├── vs_deferred_fullscreen.sc │ ├── vs_deferred_geometry.sc │ ├── vs_deferred_light.sc │ ├── vs_forward.sc │ └── vs_tonemap.sc ├── Scene ├── Camera.cpp ├── Camera.h ├── Light.cpp ├── Light.h ├── LightList.cpp ├── LightList.h ├── Material.h ├── Mesh.cpp ├── Mesh.h ├── Scene.cpp └── Scene.h ├── UI.cpp ├── UI.h └── main.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | # https://zed0.co.uk/clang-format-configurator/ 2 | # 7.0.0 (VS 2019) 3 | BasedOnStyle: WebKit 4 | AccessModifierOffset: '-4' 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: 'false' 7 | AlignConsecutiveDeclarations: 'false' 8 | AlignEscapedNewlines: Left 9 | AlignOperands: 'true' 10 | AlignTrailingComments: 'true' 11 | AllowAllParametersOfDeclarationOnNextLine: 'false' 12 | AllowShortBlocksOnASingleLine: 'false' 13 | AllowShortCaseLabelsOnASingleLine: 'false' 14 | AllowShortFunctionsOnASingleLine: Empty 15 | AllowShortIfStatementsOnASingleLine: 'false' 16 | AllowShortLoopsOnASingleLine: 'false' 17 | AlwaysBreakAfterReturnType: None 18 | AlwaysBreakBeforeMultilineStrings: 'false' 19 | AlwaysBreakTemplateDeclarations: 'Yes' 20 | BinPackArguments: 'false' 21 | BinPackParameters: 'false' 22 | BreakBeforeBinaryOperators: None 23 | BreakBeforeBraces: Allman 24 | BreakBeforeTernaryOperators: 'true' 25 | BreakConstructorInitializers: AfterColon 26 | BreakInheritanceList: AfterColon 27 | BreakStringLiterals: 'false' 28 | ColumnLimit: '120' 29 | CompactNamespaces: 'false' 30 | ConstructorInitializerAllOnOneLineOrOnePerLine: 'true' 31 | ConstructorInitializerIndentWidth: '4' 32 | ContinuationIndentWidth: '4' 33 | Cpp11BracedListStyle: 'false' 34 | DerivePointerAlignment: 'false' 35 | DisableFormat: 'false' 36 | ExperimentalAutoDetectBinPacking: 'false' 37 | FixNamespaceComments: 'true' 38 | IncludeBlocks: Merge 39 | IndentCaseLabels: 'true' 40 | IndentPPDirectives: None 41 | IndentWidth: '4' 42 | IndentWrappedFunctionNames: 'true' 43 | KeepEmptyLinesAtTheStartOfBlocks: 'false' 44 | Language: Cpp 45 | MaxEmptyLinesToKeep: '1' 46 | NamespaceIndentation: None 47 | PointerAlignment: Left 48 | ReflowComments: 'false' 49 | SortIncludes: 'false' 50 | SortUsingDeclarations: 'false' 51 | SpaceAfterCStyleCast: 'false' 52 | SpaceAfterTemplateKeyword: 'false' 53 | SpaceBeforeAssignmentOperators: 'true' 54 | SpaceBeforeCpp11BracedList: 'true' 55 | SpaceBeforeCtorInitializerColon: 'true' 56 | SpaceBeforeInheritanceColon: 'true' 57 | SpaceBeforeParens: Never 58 | SpaceBeforeRangeBasedForLoopColon: 'true' 59 | SpaceInEmptyParentheses: 'false' 60 | SpacesBeforeTrailingComments: '1' 61 | SpacesInAngles: 'false' 62 | SpacesInCStyleCastParentheses: 'false' 63 | SpacesInContainerLiterals: 'true' 64 | SpacesInParentheses: 'false' 65 | SpacesInSquareBrackets: 'false' 66 | Standard: Cpp11 67 | TabWidth: '4' 68 | UseTab: Never 69 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pezcode/Cluster/a25ff3270e8aa1818840dd00d128010fab6dc62b/.editorconfig -------------------------------------------------------------------------------- /.github/workflows/windows-build.yml: -------------------------------------------------------------------------------- 1 | name: Build CI 2 | on: [push] 3 | 4 | jobs: 5 | build: 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | os: [windows-latest] # ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | with: 13 | submodules: recursive 14 | - name: Generate 15 | run: | 16 | mkdir build 17 | cd build 18 | cmake .. 19 | cd .. 20 | - name: Build 21 | run: | 22 | cmake --build build/ --parallel --config Release 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /assets/models/ 2 | /build/ 3 | /out/ 4 | /screenshots/ 5 | /.vs/ 6 | */.vscode/ 7 | /CMakeSettings.json 8 | /*.cap 9 | /*.rdc 10 | /Nsight/ 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "3rdparty/bigg"] 2 | path = 3rdparty/bigg 3 | url = https://github.com/pezcode/bigg.git 4 | branch = master 5 | [submodule "3rdparty/IconFontCppHeaders"] 6 | path = 3rdparty/IconFontCppHeaders 7 | url = https://github.com/juliettef/IconFontCppHeaders.git 8 | [submodule "3rdparty/assimp"] 9 | path = 3rdparty/assimp 10 | url = https://github.com/assimp/assimp.git 11 | [submodule "3rdparty/spdlog"] 12 | path = 3rdparty/spdlog 13 | url = https://github.com/gabime/spdlog.git 14 | -------------------------------------------------------------------------------- /3rdparty/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # bigg (bgfx + imgui + glfw + glm) 2 | 3 | add_definitions(-DBGFX_CONFIG_RENDERER_OPENGL_MIN_VERSION=43) 4 | add_definitions(-DIMGUI_DISABLE_OBSOLETE_FUNCTIONS) 5 | set(BIGG_EXAMPLES OFF CACHE INTERNAL "") 6 | add_subdirectory(bigg) 7 | 8 | # icon font headers 9 | 10 | add_library(IconFontCppHeaders INTERFACE) 11 | target_include_directories(IconFontCppHeaders INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/IconFontCppHeaders") 12 | 13 | # assimp 14 | 15 | set(ASSIMP_NO_EXPORT ON CACHE INTERNAL "") 16 | set(ASSIMP_BUILD_SAMPLES OFF CACHE INTERNAL "") 17 | set(ASSIMP_BUILD_TESTS OFF CACHE INTERNAL "") 18 | set(ASSIMP_BUILD_ZLIB ON CACHE INTERNAL "") 19 | set(ASSIMP_BUILD_ALL_IMPORTERS_BY_DEFAULT OFF CACHE INTERNAL "") 20 | set(ASSIMP_BUILD_FBX_IMPORTER ON CACHE INTERNAL "") 21 | set(ASSIMP_BUILD_GLTF_IMPORTER ON CACHE INTERNAL "") 22 | set(ASSIMP_BUILD_OBJ_IMPORTER ON CACHE INTERNAL "") 23 | add_subdirectory(assimp) 24 | 25 | # spdlog 26 | 27 | add_subdirectory(spdlog) 28 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.2) 2 | 3 | project(cluster CXX) 4 | set(CMAKE_CXX_STANDARD 14) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | if(NOT CMAKE_BUILD_TYPE) 8 | set(CMAKE_BUILD_TYPE Release) 9 | endif() 10 | 11 | set(BUILD_SHARED_LIBS OFF) 12 | 13 | add_subdirectory(3rdparty) 14 | add_subdirectory(src) 15 | 16 | set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT Cluster) 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 pezcode 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cluster 2 | 3 | Implementation of [Clustered Shading](https://efficientshading.com/wp-content/uploads/clustered_shading_preprint.pdf) and Physically Based Rendering with the [bgfx](https://bkaradzic.github.io/bgfx/overview.html) rendering library. 4 | 5 | ![Render result](images/sponza.jpg) 6 | 7 | ![Cluster light count visualization](images/sponza_cluster_vis.png) 8 | 9 | Currently bgfx's OpenGL, DirectX 11/12 and Vulkan backends are supported. I've only tested on Windows 10 with an Nvidia GTX 1070. Other hardware or operating systems might have subtle bugs I'm not aware of. 10 | 11 | ## Functionality 12 | 13 | ### Live-switchable render paths 14 | 15 | - forward, deferred and clustered shading 16 | - output should be near-identical (as long as you don't hit the maximum light count per cluster) 17 | 18 | ### Clustered Forward Shading 19 | 20 | - [logarithmical depth partition](http://advances.realtimerendering.com/s2016/Siggraph2016_idTech6.pdf) 21 | - compute shader for cluster generation 22 | - compute shader for light culling 23 | - AABB test for point lights 24 | - cluster light count visualization 25 | 26 | ### Deferred Shading 27 | 28 | - G-Buffer with 4 render targets 29 | - diffuse RGB, roughness 30 | - [encoded view-space normal](https://aras-p.info/texts/CompactNormalStorage.html#method04spheremap) (RG16F) 31 | - F0 RGB, metallic 32 | - emissive RGB, occlusion 33 | - light culling with light geometry 34 | - axis-aligned bounding box 35 | - backface rendering with reversed depth test 36 | - fragment position reconstructed from depth buffer 37 | - final forward pass for transparent meshes 38 | 39 | ### Forward Shading 40 | 41 | Very simple implementation, might be useful to start reading the code 42 | 43 | ### Physically Based Rendering (PBR) 44 | 45 | - [metallic + roughness](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material) material support 46 | - tangent space normal mapping 47 | - Cook-Torrance microfacet specular BRDF 48 | - [GGX](https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf) normal distribution function 49 | - Smith-GGX geometric shadowing function 50 | - [multiple scattering correction](https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf) 51 | - Lambertian diffuse BRDF 52 | - [specular antialiasing](http://www.jp.square-enix.com/tech/library/pdf/ImprovedGeometricSpecularAA.pdf) 53 | - windowed light attenuation 54 | 55 | ### Tonemapping 56 | 57 | HDR tonemapping postprocessing with different operators: 58 | 59 | - exponential 60 | - simple [Reinhard](http://www.cs.utah.edu/~reinhard/cdrom/tonemap.pdf) 61 | - Reinhard ([luminance only](https://imdoingitwrong.wordpress.com/2010/08/19/why-reinhard-desaturates-my-blacks-3/)) 62 | - [Uncharted 2](https://www.slideshare.net/ozlael/hable-john-uncharted2-hdr-lighting) 63 | - [H.P. Duiker filmic curve](https://www.slideshare.net/hpduiker/filmic-tonemapping-for-realtime-rendering-siggraph-2010-color-course) 64 | - [ACES](https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl) 65 | - ACES ([luminance only](https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/)) 66 | 67 | ## References 68 | 69 | A few useful resources that helped with the implementation: 70 | 71 | - Clustered Shading: 72 | - [A Primer On Efficient Rendering Algorithms & Clustered Shading](http://www.aortiz.me/2018/12/21/CG.html) by Angel Ortiz 73 | - [Practical Clustered Shading](https://newq.net/dl/pub/s2015_practical.pdf) by Emil Persson 74 | - [The devil is in the details: idTech 666](http://advances.realtimerendering.com/s2016/Siggraph2016_idTech6.pdf) by Tiago Sousa und Jean Geffroy 75 | - Physically Based Rendering: 76 | - [Physically Based Rendering in Filament](https://google.github.io/filament/Filament.md.html) by Romain Guy and Mathias Agopian 77 | - [Real Shading in Unreal Engine 4](https://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf) by Brian Karis 78 | - [Moving Frostbite to Physically Based Rendering](https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf) by Sébastien Lagarde and Charles de Rousiers 79 | - Tonemapping: 80 | - [Filmic Tonemapping Operators](http://filmicworlds.com/blog/filmic-tonemapping-operators/) by John Hable 81 | 82 | ## Compilation 83 | 84 | [CMake](https://cmake.org/) (>= 3.2) is required for building. 85 | 86 | 1. Generate project files: 87 | ```bash 88 | mkdir build 89 | cd build 90 | # e.g. VS 2019, compile for x64 platform 91 | cmake -G "Visual Studio 16 2019" -A x64 .. 92 | cd .. 93 | ``` 94 | 95 | 2. Build. Open the project files with your IDE/build tool, or use CMake: 96 | ``` 97 | cmake --build build/ --parallel --config Release 98 | ``` 99 | 100 | You can also grab a compiled copy for Windows with the Sponza model from the [Releases](https://github.com/pezcode/Cluster/releases) page. 101 | 102 | ## Libraries 103 | 104 | - [bigg](https://github.com/JoshuaBrookover/bigg) ([bgfx](https://bkaradzic.github.io/bgfx/overview.html) + [dear imgui](https://github.com/ocornut/imgui) + [glfw](https://www.glfw.org) + [glm](https://glm.g-truc.net)) for UI and rendering 105 | - [IconFontCppHeaders](https://github.com/juliettef/IconFontCppHeaders) for icon font support 106 | - [assimp](http://www.assimp.org/) for model import 107 | - [spdlog](https://github.com/gabime/spdlog) for logging 108 | 109 | ## Assets 110 | 111 | - [Sponza](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/Sponza) model 112 | - [Roboto](https://fonts.google.com/specimen/Roboto) font 113 | - [Fork Awesome](https://forkaweso.me/Fork-Awesome/) icon font 114 | 115 | ## License 116 | 117 | This software is licensed under the [MIT License](https://choosealicense.com/licenses/mit). Basically, you can do whatever you want with it, as long as you don't remove the license and copyright notice from relevant pieces of code. 118 | -------------------------------------------------------------------------------- /assets/fonts/ForkAwesome/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Fork Awesome (https://forkawesome.github.io), 2 | with Reserved Font Name Fork Awesome. 3 | 4 | 5 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 6 | This license is copied below, and is also available with a FAQ at: 7 | http://scripts.sil.org/OFL 8 | 9 | 10 | ----------------------------------------------------------- 11 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 12 | ----------------------------------------------------------- 13 | 14 | PREAMBLE 15 | The goals of the Open Font License (OFL) are to stimulate worldwide 16 | development of collaborative font projects, to support the font creation 17 | efforts of academic and linguistic communities, and to provide a free and 18 | open framework in which fonts may be shared and improved in partnership 19 | with others. 20 | 21 | The OFL allows the licensed fonts to be used, studied, modified and 22 | redistributed freely as long as they are not sold by themselves. The 23 | fonts, including any derivative works, can be bundled, embedded, 24 | redistributed and/or sold with any software provided that any reserved 25 | names are not used by derivative works. The fonts and derivatives, 26 | however, cannot be released under any other type of license. The 27 | requirement for fonts to remain under this license does not apply 28 | to any document created using the fonts or their derivatives. 29 | 30 | DEFINITIONS 31 | "Font Software" refers to the set of files released by the Copyright 32 | Holder(s) under this license and clearly marked as such. This may 33 | include source files, build scripts and documentation. 34 | 35 | "Reserved Font Name" refers to any names specified as such after the 36 | copyright statement(s). 37 | 38 | "Original Version" refers to the collection of Font Software components as 39 | distributed by the Copyright Holder(s). 40 | 41 | "Modified Version" refers to any derivative made by adding to, deleting, 42 | or substituting -- in part or in whole -- any of the components of the 43 | Original Version, by changing formats or by porting the Font Software to a 44 | new environment. 45 | 46 | "Author" refers to any designer, engineer, programmer, technical 47 | writer or other person who contributed to the Font Software. 48 | 49 | PERMISSION & CONDITIONS 50 | Permission is hereby granted, free of charge, to any person obtaining 51 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 52 | redistribute, and sell modified and unmodified copies of the Font 53 | Software, subject to the following conditions: 54 | 55 | 1) Neither the Font Software nor any of its individual components, 56 | in Original or Modified Versions, may be sold by itself. 57 | 58 | 2) Original or Modified Versions of the Font Software may be bundled, 59 | redistributed and/or sold with any software, provided that each copy 60 | contains the above copyright notice and this license. These can be 61 | included either as stand-alone text files, human-readable headers or 62 | in the appropriate machine-readable metadata fields within text or 63 | binary files as long as those fields can be easily viewed by the user. 64 | 65 | 3) No Modified Version of the Font Software may use the Reserved Font 66 | Name(s) unless explicit written permission is granted by the corresponding 67 | Copyright Holder. This restriction only applies to the primary font name as 68 | presented to the users. 69 | 70 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 71 | Software shall not be used to promote, endorse or advertise any 72 | Modified Version, except to acknowledge the contribution(s) of the 73 | Copyright Holder(s) and the Author(s) or with their explicit written 74 | permission. 75 | 76 | 5) The Font Software, modified or unmodified, in part or in whole, 77 | must be distributed entirely under this license, and must not be 78 | distributed under any other license. The requirement for fonts to 79 | remain under this license does not apply to any document created 80 | using the Font Software. 81 | 82 | TERMINATION 83 | This license becomes null and void if any of the above conditions are 84 | not met. 85 | 86 | DISCLAIMER 87 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 88 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 89 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 90 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 91 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 92 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 93 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 94 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 95 | OTHER DEALINGS IN THE FONT SOFTWARE. 96 | -------------------------------------------------------------------------------- /assets/fonts/ForkAwesome/forkawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pezcode/Cluster/a25ff3270e8aa1818840dd00d128010fab6dc62b/assets/fonts/ForkAwesome/forkawesome-webfont.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /assets/fonts/Roboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pezcode/Cluster/a25ff3270e8aa1818840dd00d128010fab6dc62b/assets/fonts/Roboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /images/sponza.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pezcode/Cluster/a25ff3270e8aa1818840dd00d128010fab6dc62b/images/sponza.jpg -------------------------------------------------------------------------------- /images/sponza_cluster_vis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pezcode/Cluster/a25ff3270e8aa1818840dd00d128010fab6dc62b/images/sponza_cluster_vis.png -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(SOURCES 2 | main.cpp 3 | Cluster.h 4 | Cluster.cpp 5 | Config.h 6 | Config.cpp 7 | UI.h 8 | UI.cpp 9 | 10 | Log/Log.h 11 | Log/Log.cpp 12 | Log/UISink.h 13 | Log/AssimpSource.h 14 | 15 | Renderer/Renderer.h 16 | Renderer/Renderer.cpp 17 | Renderer/ForwardRenderer.h 18 | Renderer/ForwardRenderer.cpp 19 | Renderer/DeferredRenderer.h 20 | Renderer/DeferredRenderer.cpp 21 | Renderer/ClusteredRenderer.h 22 | Renderer/ClusteredRenderer.cpp 23 | Renderer/PBRShader.h 24 | Renderer/PBRShader.cpp 25 | Renderer/LightShader.h 26 | Renderer/LightShader.cpp 27 | Renderer/ClusterShader.h 28 | Renderer/ClusterShader.cpp 29 | Renderer/Samplers.h 30 | 31 | Scene/Scene.h 32 | Scene/Scene.cpp 33 | Scene/Camera.h 34 | Scene/Camera.cpp 35 | Scene/Mesh.h 36 | Scene/Mesh.cpp 37 | Scene/Material.h 38 | Scene/Light.h 39 | Scene/Light.cpp 40 | Scene/LightList.h 41 | Scene/LightList.cpp 42 | ) 43 | 44 | set(SHADERS 45 | Renderer/Shaders/varying.def.sc 46 | Renderer/Shaders/cs_multiple_scattering_lut.sc 47 | Renderer/Shaders/vs_clustered.sc 48 | Renderer/Shaders/fs_clustered.sc 49 | Renderer/Shaders/fs_clustered_debug_vis.sc 50 | Renderer/Shaders/cs_clustered_clusterbuilding.sc 51 | Renderer/Shaders/cs_clustered_reset_counter.sc 52 | Renderer/Shaders/cs_clustered_lightculling.sc 53 | Renderer/Shaders/vs_deferred_geometry.sc 54 | Renderer/Shaders/fs_deferred_geometry.sc 55 | Renderer/Shaders/vs_deferred_light.sc 56 | Renderer/Shaders/fs_deferred_pointlight.sc 57 | Renderer/Shaders/vs_deferred_fullscreen.sc 58 | Renderer/Shaders/fs_deferred_fullscreen.sc 59 | Renderer/Shaders/vs_forward.sc 60 | Renderer/Shaders/fs_forward.sc 61 | Renderer/Shaders/vs_tonemap.sc 62 | Renderer/Shaders/fs_tonemap.sc 63 | Renderer/Shaders/samplers.sh 64 | Renderer/Shaders/tonemapping.sh 65 | Renderer/Shaders/pbr.sh 66 | Renderer/Shaders/lights.sh 67 | Renderer/Shaders/clusters.sh 68 | Renderer/Shaders/colormap.sh 69 | Renderer/Shaders/util.sh 70 | ) 71 | 72 | if(MSVC) 73 | # hide console window on Windows 74 | # for some reason this still requires WIN32 in add_executable to work 75 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup") 76 | set(PLATFORM WIN32) 77 | endif() 78 | 79 | add_executable(Cluster ${PLATFORM} ${SOURCES} ${SHADERS}) 80 | target_include_directories(Cluster PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) 81 | target_link_libraries(Cluster PRIVATE bigg IconFontCppHeaders assimp spdlog) 82 | target_compile_definitions(Cluster PRIVATE 83 | IMGUI_DISABLE_OBSOLETE_FUNCTIONS 84 | # enable SIMD optimizations 85 | GLM_FORCE_INTRINSICS 86 | # inline code where possible 87 | GLM_FORCE_INLINE 88 | # no implicit conversions 89 | GLM_FORCE_EXPLICIT_CTOR 90 | # length() returns size_t instead of int 91 | GLM_FORCE_SIZE_T_LENGTH 92 | ) 93 | 94 | set_target_properties(Cluster PROPERTIES 95 | RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}" 96 | ) 97 | 98 | configure_debugging(Cluster WORKING_DIR ${PROJECT_BINARY_DIR}) 99 | 100 | if(MSVC) 101 | # disable macro redefinition warning 102 | # ideally 3rd party include folders were marked as SYSTEM so we wouldn't get these warnings 103 | target_compile_options(Cluster PRIVATE "/wd4005") 104 | endif() 105 | 106 | set(SHADER_DIR "${PROJECT_BINARY_DIR}/shaders") 107 | set(ASSETS_DIR "${PROJECT_BINARY_DIR}/assets") 108 | 109 | # target to force all shaders to recompile on the next build 110 | # sometimes handy after changing included header files (.sh) 111 | # since add_shader doesn't take those dependencies into account 112 | add_custom_target(invalidate_shaders) 113 | 114 | foreach(SHADER ${SHADERS}) 115 | get_filename_component(SHADER_NAME "${SHADER}" NAME) 116 | get_filename_component(SHADER_FILE "${SHADER}" ABSOLUTE) 117 | # add_shader defaults to GLSL 120, 430 for compute 118 | # GLSL 1.30 is needed for switch statements but shaderc produces shader 119 | # binaries without any #version directive unless we request 4.00 or higher 120 | # this makes shader compilation fail with no error message 121 | # just use 4.30 for all shaders 122 | # default DX11 model is 5_0 123 | set(GLSL_VERSION 430) 124 | set(GLSL_COMPUTE_VERSION 430) 125 | set(DX_MODEL 5_0) 126 | 127 | # DX9/11 shaders can only be compiled on Windows 128 | set(SHADER_PLATFORMS glsl spirv) 129 | if(WIN32) 130 | set(SHADER_PLATFORMS ${SHADER_PLATFORMS} dx11) 131 | endif() 132 | 133 | if(SHADER_NAME MATCHES "^vs_") 134 | add_shader("${SHADER_FILE}" VERTEX 135 | OUTPUT "${SHADER_DIR}" 136 | GLSL_VERSION ${GLSL_VERSION} 137 | DX11_MODEL ${DX_MODEL} 138 | PLATFORMS ${SHADER_PLATFORMS}) 139 | elseif(SHADER_NAME MATCHES "^fs_") 140 | add_shader("${SHADER_FILE}" FRAGMENT 141 | OUTPUT "${SHADER_DIR}" 142 | GLSL_VERSION ${GLSL_VERSION} 143 | DX11_MODEL ${DX_MODEL} 144 | PLATFORMS ${SHADER_PLATFORMS}) 145 | elseif(SHADER_NAME MATCHES "^cs_") 146 | add_shader("${SHADER_FILE}" COMPUTE 147 | OUTPUT "${SHADER_DIR}" 148 | GLSL_VERSION ${GLSL_COMPUTE_VERSION} 149 | DX11_MODEL ${DX_MODEL} 150 | PLATFORMS ${SHADER_PLATFORMS}) 151 | endif() 152 | add_custom_command(TARGET invalidate_shaders PRE_BUILD 153 | COMMAND "${CMAKE_COMMAND}" -E touch "${SHADER_FILE}") 154 | endforeach() 155 | 156 | # add_shader does this, do it manually for includes/varying.def.sc 157 | source_group("Shader Files" FILES ${SHADERS}) 158 | 159 | file(COPY ../assets/ DESTINATION ${ASSETS_DIR}) 160 | 161 | install(TARGETS Cluster RUNTIME DESTINATION bin) 162 | install(DIRECTORY ${SHADER_DIR} DESTINATION bin) 163 | install(DIRECTORY ${ASSETS_DIR} DESTINATION bin) 164 | -------------------------------------------------------------------------------- /src/Cluster.cpp: -------------------------------------------------------------------------------- 1 | #include "Cluster.h" 2 | 3 | #include "UI.h" 4 | #include "Config.h" 5 | #include "Scene/Scene.h" 6 | #include "Log/Log.h" 7 | #include "Renderer/ForwardRenderer.h" 8 | #include "Renderer/DeferredRenderer.h" 9 | #include "Renderer/ClusteredRenderer.h" 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | Cluster::Cluster() : 20 | bigg::Application("Cluster", 1280, 720), 21 | callbacks(*this), 22 | config(std::make_unique()), 23 | ui(std::make_unique(*this)), 24 | scene(std::make_unique()) 25 | { 26 | } 27 | 28 | Cluster::~Cluster() 29 | { 30 | // we need to define the destructor where the forward-declared classes are actually defined by now 31 | // otherwise we get a default constructor defined in the header 32 | // then unique_ptr can't delete the object and compilation fails 33 | } 34 | 35 | int Cluster::run(int argc, char* argv[]) 36 | { 37 | config->readArgv(argc, argv); 38 | 39 | return Application::run(argc, argv, config->renderer, BGFX_PCI_ID_NONE, 0, &callbacks, nullptr); 40 | } 41 | 42 | void Cluster::initialize(int _argc, char* _argv[]) 43 | { 44 | if(config->writeLog) 45 | { 46 | // _mt (thread safe) necessary because of flush_every 47 | logFileSink = std::make_shared(config->logFile, true); 48 | logFileSink->set_level(spdlog::level::trace); 49 | logFileSink->set_pattern("[%H:%M:%S][%l] %v"); 50 | Sinks->add_sink(logFileSink); 51 | } 52 | 53 | Log->flush_on(spdlog::level::info); 54 | Log->set_level(spdlog::level::trace); 55 | spdlog::flush_every(std::chrono::seconds(2)); 56 | 57 | if(!ForwardRenderer::supported()) 58 | { 59 | Log->error("Forward renderer not supported on this hardware"); 60 | close(); 61 | return; 62 | } 63 | if(!DeferredRenderer::supported()) 64 | { 65 | Log->error("Deferred renderer not supported on this hardware"); 66 | close(); 67 | return; 68 | } 69 | if(!ClusteredRenderer::supported()) 70 | { 71 | Log->error("Clustered renderer not supported on this hardware"); 72 | close(); 73 | return; 74 | } 75 | 76 | if(config->profile) 77 | bgfx::setDebug(BGFX_DEBUG_PROFILER); 78 | 79 | uint32_t resetFlags = BGFX_RESET_MAXANISOTROPY | BGFX_RESET_SRGB_BACKBUFFER; 80 | if(config->vsync) 81 | resetFlags |= BGFX_RESET_VSYNC; 82 | reset(resetFlags); 83 | 84 | if(config->fullscreen) 85 | toggleFullscreen(); 86 | 87 | if(glfwRawMouseMotionSupported()) 88 | glfwSetInputMode(mWindow, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); 89 | 90 | // renderer has already been created in onReset 91 | renderer->setTonemappingMode(config->tonemappingMode); 92 | renderer->setMultipleScattering(config->multipleScattering); 93 | ui->initialize(); 94 | 95 | Scene::init(); 96 | 97 | if(!scene->load(config->sceneFile)) 98 | { 99 | Log->error("Loading scene model failed"); 100 | close(); 101 | return; 102 | } 103 | 104 | // Sponza debug camera + lights 105 | if(!config->customScene) 106 | { 107 | scene->camera.lookAt({ -7.0f, 2.0f, 0.0f }, scene->center, glm::vec3(0.0f, 1.0f, 0.0f)); 108 | scene->pointLights.lights = { // pos, power 109 | { { -5.0f, 0.3f, 0.0f }, { 100.0f, 100.0f, 100.0f } }, 110 | { { 0.0f, 0.3f, 0.0f }, { 100.0f, 100.0f, 100.0f } }, 111 | { { 5.0f, 0.3f, 0.0f }, { 100.0f, 100.0f, 100.0f } } 112 | }; 113 | } 114 | 115 | scene->pointLights.update(); 116 | config->lights = (int)scene->pointLights.lights.size(); 117 | } 118 | 119 | void Cluster::onReset() 120 | { 121 | // init renderer here 122 | // onReset is called before initialize on startup 123 | if(!renderer) 124 | setRenderPath(config->renderPath); 125 | 126 | renderer->reset(uint16_t(getWidth()), uint16_t(getHeight())); 127 | } 128 | 129 | void Cluster::onKey(int key, int scancode, int action, int mods) 130 | { 131 | if(action == GLFW_RELEASE) 132 | { 133 | switch(key) 134 | { 135 | case GLFW_KEY_ESCAPE: 136 | close(); 137 | break; 138 | case GLFW_KEY_R: 139 | config->showUI = true; 140 | config->showConfigWindow = true; 141 | break; 142 | } 143 | } 144 | } 145 | 146 | void Cluster::onCursorPos(double xpos, double ypos) 147 | { 148 | constexpr float angularVelocity = 180.0f / 600.0f; // degrees/pixel 149 | 150 | if(mouseX >= 0.0f && mouseY >= 0.0f) 151 | { 152 | if(isMouseButtonDown(GLFW_MOUSE_BUTTON_RIGHT)) 153 | { 154 | glfwSetInputMode(mWindow, GLFW_CURSOR, GLFW_CURSOR_HIDDEN); 155 | scene->camera.rotate(-glm::vec2(ypos - mouseY, xpos - mouseX) * angularVelocity); 156 | } 157 | else 158 | { 159 | glfwSetInputMode(mWindow, GLFW_CURSOR, GLFW_CURSOR_NORMAL); 160 | } 161 | } 162 | mouseX = xpos; 163 | mouseY = ypos; 164 | } 165 | 166 | void Cluster::onCursorEnter(int entered) 167 | { 168 | if(!entered) // lost focus 169 | { 170 | mouseX = mouseY = -1.0f; 171 | } 172 | } 173 | 174 | void Cluster::onScroll(double xoffset, double yoffset) 175 | { 176 | // wheel scrolled up = zoom in by 2 extra degrees 177 | scene->camera.zoom((float)yoffset * 2.0f); 178 | } 179 | 180 | void Cluster::update(float dt) 181 | { 182 | const float t = (float)glfwGetTime(); 183 | 184 | float velocity = scene->diagonal / 5.0f; // m/s 185 | if(isKeyDown(GLFW_KEY_W)) 186 | scene->camera.move(scene->camera.forward() * velocity * dt); 187 | if(isKeyDown(GLFW_KEY_A)) 188 | scene->camera.move(-scene->camera.right() * velocity * dt); 189 | if(isKeyDown(GLFW_KEY_S)) 190 | scene->camera.move(-scene->camera.forward() * velocity * dt); 191 | if(isKeyDown(GLFW_KEY_D)) 192 | scene->camera.move(scene->camera.right() * velocity * dt); 193 | if(isKeyDown(GLFW_KEY_SPACE)) 194 | scene->camera.move(scene->camera.up() * velocity * dt); 195 | if(isKeyDown(GLFW_KEY_LEFT_CONTROL)) 196 | scene->camera.move(-scene->camera.up() * velocity * dt); 197 | 198 | if(config->movingLights) 199 | moveLights(t, dt); 200 | scene->pointLights.update(); 201 | 202 | renderer->render(dt); 203 | ui->update(dt); 204 | } 205 | 206 | int Cluster::shutdown() 207 | { 208 | ui->shutdown(); 209 | renderer->shutdown(); 210 | scene->clear(); 211 | Sinks->remove_sink(logFileSink); 212 | logFileSink = nullptr; 213 | return 0; 214 | } 215 | 216 | void Cluster::BgfxCallbacks::fatal(const char* filePath, uint16_t line, bgfx::Fatal::Enum code, const char* str) 217 | { 218 | if(code != bgfx::Fatal::DebugCheck) 219 | { 220 | // unrecoverable, terminate 221 | // don't log debug checks either, their output gets sent to traceVargs 222 | Log->critical("{}", str); 223 | app.close(); 224 | } 225 | } 226 | 227 | // only called when compiled as debug 228 | void Cluster::BgfxCallbacks::traceVargs(const char* filePath, uint16_t line, const char* format, va_list args) 229 | { 230 | char buffer[1024]; 231 | int32_t written = bx::vsnprintf(buffer, BX_COUNTOF(buffer), format, args); 232 | if(written > 0 && written < BX_COUNTOF(buffer)) 233 | { 234 | // bgfx sends lines with newlines, spdlog adds another 235 | if(buffer[written - 1] == '\n') 236 | buffer[written - 1] = '\0'; 237 | Log->trace(buffer); 238 | } 239 | } 240 | 241 | void Cluster::close() 242 | { 243 | glfwSetWindowShouldClose(mWindow, 1); 244 | } 245 | 246 | void Cluster::toggleFullscreen() 247 | { 248 | static int oldX = 0, oldY = 0; 249 | static int oldWidth = 0, oldHeight = 0; 250 | 251 | // GLFW didn't create the context, bgfx did 252 | // glfwGetCurrentContext returns null 253 | GLFWwindow* window = mWindow; //glfwGetCurrentContext(); 254 | if(glfwGetWindowMonitor(window)) 255 | { 256 | glfwSetWindowMonitor(window, NULL, oldX, oldY, oldWidth, oldHeight, 0); 257 | config->fullscreen = false; 258 | } 259 | else 260 | { 261 | GLFWmonitor* monitor = glfwGetPrimaryMonitor(); 262 | if(NULL != monitor) 263 | { 264 | glfwGetWindowPos(window, &oldX, &oldY); 265 | glfwGetWindowSize(window, &oldWidth, &oldHeight); 266 | 267 | const GLFWvidmode* mode = glfwGetVideoMode(monitor); 268 | glfwSetWindowMonitor(window, monitor, 0, 0, mode->width, mode->height, mode->refreshRate); 269 | config->fullscreen = true; 270 | } 271 | } 272 | } 273 | 274 | void Cluster::setRenderPath(RenderPath path) 275 | { 276 | if(renderer && path == config->renderPath) 277 | return; 278 | 279 | if(renderer) 280 | renderer->shutdown(); 281 | renderer.release(); 282 | 283 | switch(path) 284 | { 285 | case RenderPath::Forward: 286 | renderer = std::make_unique(scene.get()); 287 | break; 288 | case RenderPath::Deferred: 289 | renderer = std::make_unique(scene.get()); 290 | break; 291 | case RenderPath::Clustered: 292 | renderer = std::make_unique(scene.get()); 293 | break; 294 | default: 295 | assert(false); 296 | break; 297 | } 298 | 299 | renderer->reset(getWidth(), getHeight()); 300 | renderer->initialize(); 301 | 302 | config->renderPath = path; 303 | } 304 | 305 | void Cluster::generateLights(unsigned int count) 306 | { 307 | // TODO? normalize power 308 | 309 | auto& lights = scene->pointLights.lights; 310 | 311 | size_t keep = lights.size(); 312 | if(count < keep) 313 | keep = count; 314 | 315 | lights.resize(count); 316 | 317 | glm::vec3 scale = glm::abs(scene->maxBounds - scene->minBounds) * 0.75f; 318 | 319 | constexpr float POWER_MIN = 20.0f; 320 | constexpr float POWER_MAX = 100.0f; 321 | 322 | std::random_device rd; 323 | std::seed_seq seed = { rd() }; 324 | std::mt19937 mt(seed); 325 | std::uniform_real_distribution dist(0.0f, 1.0f); 326 | 327 | for(size_t i = keep; i < count; i++) 328 | { 329 | glm::vec3 position = scene->center; 330 | position += glm::vec3(dist(mt), dist(mt), dist(mt)) * scale - (scale * 0.5f); 331 | 332 | if(!config->customScene) // Sponza, no lights under the floor 333 | position.y = glm::abs(position.y); 334 | 335 | glm::vec3 color = glm::vec3(dist(mt), dist(mt), dist(mt)); 336 | glm::vec3 power = color * (dist(mt) * (POWER_MAX - POWER_MIN) + POWER_MIN); 337 | lights[i] = { position, power }; 338 | } 339 | } 340 | 341 | void Cluster::moveLights(float t, float dt) 342 | { 343 | const float angularVelocity = glm::radians(10.0f); 344 | const float angle = angularVelocity * dt; 345 | //const glm::vec3 translationExtent = glm::abs(scene->maxBounds - scene->minBounds) * glm::vec3( 0.1f, 0.0f, 0.1f ); // { 1.0f, 0.0f, 1.0f }; 346 | 347 | for(PointLight& light : scene->pointLights.lights) 348 | { 349 | light.position = 350 | glm::mat3(glm::rotate(glm::identity(), angle, glm::vec3(0.0f, 1.0f, 0.0f))) * light.position; 351 | //light.position += glm::sin(glm::vec3(t) * glm::vec3(1.0f, 2.0f, 3.0f)) * translationExtent * dt; 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/Cluster.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class ClusterUI; 10 | class Config; 11 | class Scene; 12 | class Renderer; 13 | 14 | class Cluster : public bigg::Application 15 | { 16 | friend class ClusterUI; 17 | 18 | public: 19 | Cluster(); 20 | ~Cluster(); 21 | 22 | int run(int argc, char* argv[]); 23 | 24 | // bigg callbacks 25 | 26 | void initialize(int _argc, char* _argv[]) override; 27 | void onReset() override; 28 | void onKey(int key, int scancode, int action, int mods) override; 29 | void onCursorPos(double xpos, double ypos) override; 30 | void onCursorEnter(int entered) override; 31 | void onScroll(double xoffset, double yoffset) override; 32 | void update(float dt) override; 33 | int shutdown() override; 34 | 35 | // 36 | 37 | void close(); 38 | void toggleFullscreen(); 39 | 40 | enum class RenderPath : int 41 | { 42 | Forward, 43 | Deferred, 44 | Clustered 45 | }; 46 | void setRenderPath(RenderPath path); 47 | 48 | void generateLights(unsigned int count); 49 | void moveLights(float t, float dt); 50 | 51 | private: 52 | class BgfxCallbacks : public bgfx::CallbackI 53 | { 54 | public: 55 | BgfxCallbacks(Cluster& app) : app(app) { } 56 | virtual ~BgfxCallbacks() { } 57 | virtual void fatal(const char*, uint16_t, bgfx::Fatal::Enum, const char*) override; 58 | virtual void traceVargs(const char*, uint16_t, const char*, va_list) override; 59 | virtual void profilerBegin(const char*, uint32_t, const char*, uint16_t) override { } 60 | virtual void profilerBeginLiteral(const char*, uint32_t, const char*, uint16_t) override { } 61 | virtual void profilerEnd() override { } 62 | virtual uint32_t cacheReadSize(uint64_t) override 63 | { 64 | return 0; 65 | } 66 | virtual bool cacheRead(uint64_t, void*, uint32_t) override 67 | { 68 | return false; 69 | } 70 | virtual void cacheWrite(uint64_t, const void*, uint32_t) override { } 71 | virtual void captureBegin(uint32_t, uint32_t, uint32_t, bgfx::TextureFormat::Enum, bool) override { } 72 | virtual void captureEnd() override { } 73 | virtual void captureFrame(const void*, uint32_t) override { } 74 | virtual void screenShot(const char*, uint32_t, uint32_t, uint32_t, const void*, uint32_t, bool yflip) override 75 | { 76 | } 77 | 78 | private: 79 | Cluster& app; 80 | }; 81 | 82 | spdlog::sink_ptr logFileSink = nullptr; 83 | 84 | double mouseX = -1.0, mouseY = -1.0; 85 | 86 | BgfxCallbacks callbacks; 87 | 88 | // pointers to avoid circular dependencies 89 | // Cluster has Config member, Config needs enum definitions 90 | 91 | std::unique_ptr config; 92 | std::unique_ptr ui; 93 | std::unique_ptr scene; 94 | 95 | std::unique_ptr renderer; 96 | }; 97 | -------------------------------------------------------------------------------- /src/Config.cpp: -------------------------------------------------------------------------------- 1 | #include "Config.h" 2 | 3 | #include 4 | #include "Renderer/Renderer.h" 5 | 6 | Config::Config() : 7 | writeLog(true), 8 | logFile("Cluster.log"), 9 | renderer(bgfx::RendererType::Count), // default renderer, chosen by platform 10 | renderPath(Cluster::RenderPath::Clustered), 11 | tonemappingMode(Renderer::TonemappingMode::ACES), 12 | multipleScattering(true), 13 | whiteFurnace(false), 14 | profile(true), 15 | vsync(false), 16 | sceneFile("assets/models/Sponza/Sponza.gltf"), 17 | customScene(false), 18 | lights(1), 19 | maxLights(3000), 20 | movingLights(false), 21 | fullscreen(false), 22 | showUI(true), 23 | showConfigWindow(true), 24 | showLog(false), 25 | showStatsOverlay(false), 26 | overlays({ true, true, true, true }), 27 | showBuffers(false), 28 | debugVisualization(false) 29 | { 30 | } 31 | 32 | void Config::readArgv(int argc, char* argv[]) 33 | { 34 | // argv must outlive Config 35 | // we store pointers into argv for the scene file 36 | bx::CommandLine cmdLine(argc, argv); 37 | 38 | if(cmdLine.hasArg("noop")) 39 | renderer = bgfx::RendererType::Noop; 40 | else if(cmdLine.hasArg("gl")) 41 | renderer = bgfx::RendererType::OpenGL; 42 | else if(cmdLine.hasArg("vk")) 43 | renderer = bgfx::RendererType::Vulkan; 44 | // missing required features 45 | else if(cmdLine.hasArg("d3d9")) 46 | renderer = bgfx::RendererType::Direct3D9; 47 | else if(cmdLine.hasArg("d3d11")) 48 | renderer = bgfx::RendererType::Direct3D11; 49 | else if(cmdLine.hasArg("d3d12")) 50 | renderer = bgfx::RendererType::Direct3D12; 51 | // not tested 52 | else if(cmdLine.hasArg("mtl")) 53 | renderer = bgfx::RendererType::Metal; 54 | 55 | const char* scene = cmdLine.findOption("scene"); 56 | if(scene) 57 | { 58 | sceneFile = scene; 59 | customScene = true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "Renderer/Renderer.h" 5 | #include "Cluster.h" 6 | 7 | class Config 8 | { 9 | public: 10 | Config(); 11 | 12 | void readArgv(int argc, char* argv[]); 13 | 14 | // * = not exposed to UI 15 | 16 | // Log 17 | 18 | bool writeLog; // * 19 | const char* logFile; // * 20 | 21 | // Renderer 22 | 23 | bgfx::RendererType::Enum renderer; // * 24 | Cluster::RenderPath renderPath; 25 | Renderer::TonemappingMode tonemappingMode; 26 | 27 | bool multipleScattering; 28 | bool whiteFurnace; 29 | 30 | bool profile; // enable bgfx view profiling * 31 | bool vsync; // * 32 | 33 | // Scene 34 | 35 | const char* sceneFile; // gltf file to load * 36 | bool customScene; // not the standard Sponza scene, don't place debug lights/camera * 37 | int lights; 38 | int maxLights; // * 39 | bool movingLights; 40 | 41 | // UI 42 | 43 | bool fullscreen; 44 | bool showUI; 45 | bool showConfigWindow; 46 | bool showLog; 47 | 48 | bool showStatsOverlay; 49 | struct 50 | { 51 | bool fps; 52 | bool frameTime; 53 | bool profiler; 54 | bool gpuMemory; 55 | } overlays; 56 | 57 | bool showBuffers; 58 | // clustered renderer 59 | bool debugVisualization; 60 | }; 61 | -------------------------------------------------------------------------------- /src/Log/AssimpSource.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "Log.h" 6 | 7 | class AssimpLogSource : public Assimp::Logger 8 | { 9 | virtual void OnVerboseDebug(const char* message) override 10 | { 11 | Log->trace(message); 12 | } 13 | virtual void OnDebug(const char* message) override 14 | { 15 | Log->debug(message); 16 | } 17 | virtual void OnInfo(const char* message) override 18 | { 19 | Log->info(message); 20 | } 21 | virtual void OnWarn(const char* message) override 22 | { 23 | Log->warn(message); 24 | } 25 | virtual void OnError(const char* message) override 26 | { 27 | Log->error(message); 28 | } 29 | 30 | virtual bool attachStream(Assimp::LogStream* pStream, unsigned int severity) override 31 | { 32 | delete pStream; 33 | return true; 34 | } 35 | virtual bool detachStream(Assimp::LogStream* pStream, unsigned int severity) override 36 | { 37 | return true; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/Log/Log.cpp: -------------------------------------------------------------------------------- 1 | #include "Log.h" 2 | 3 | std::shared_ptr Sinks = std::make_shared(); 4 | std::shared_ptr Log = std::make_shared("Cluster", Sinks); 5 | -------------------------------------------------------------------------------- /src/Log/Log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | // global log object 8 | // initially empty 9 | // any part of the code can add sinks to Sinks 10 | extern std::shared_ptr Log; 11 | // multithreaded 12 | // (required for flush_every) 13 | extern std::shared_ptr Sinks; 14 | -------------------------------------------------------------------------------- /src/Log/UISink.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace spdlog 8 | { 9 | namespace ext 10 | { 11 | template 12 | class clusterui_sink : public sinks::base_sink 13 | { 14 | public: 15 | clusterui_sink(Func func) : func(func) { } 16 | 17 | protected: 18 | Func func; 19 | 20 | virtual void sink_it_(const details::log_msg& msg) override 21 | { 22 | // msg.payload is the raw string without any formatting 23 | memory_buf_t formatted; 24 | this->formatter_->format(msg, formatted); 25 | func(fmt::to_string(formatted).c_str(), msg.level); 26 | } 27 | 28 | virtual void flush_() override { } 29 | }; 30 | 31 | template 32 | using clusterui_sink_mt = clusterui_sink; 33 | template 34 | using clusterui_sink_st = clusterui_sink; 35 | 36 | } // namespace ext 37 | } // namespace spdlog 38 | -------------------------------------------------------------------------------- /src/Renderer/ClusterShader.cpp: -------------------------------------------------------------------------------- 1 | #include "ClusterShader.h" 2 | 3 | #include "Scene/Scene.h" 4 | #include "Renderer/Samplers.h" 5 | #include 6 | #include 7 | #include 8 | 9 | bgfx::VertexLayout ClusterShader::ClusterVertex::layout; 10 | 11 | ClusterShader::ClusterShader() 12 | { 13 | static_assert(CLUSTERS_Z % CLUSTERS_Z_THREADS == 0, 14 | "number of cluster depth slices must be divisible by thread count z-dimension"); 15 | } 16 | 17 | void ClusterShader::initialize() 18 | { 19 | ClusterVertex::init(); 20 | 21 | clusterSizesVecUniform = bgfx::createUniform("u_clusterSizesVec", bgfx::UniformType::Vec4); 22 | zNearFarVecUniform = bgfx::createUniform("u_zNearFarVec", bgfx::UniformType::Vec4); 23 | 24 | clustersBuffer = 25 | bgfx::createDynamicVertexBuffer(CLUSTER_COUNT, ClusterVertex::layout, BGFX_BUFFER_COMPUTE_READ_WRITE); 26 | lightIndicesBuffer = bgfx::createDynamicIndexBuffer(CLUSTER_COUNT * MAX_LIGHTS_PER_CLUSTER, 27 | BGFX_BUFFER_COMPUTE_READ_WRITE | BGFX_BUFFER_INDEX32); 28 | // we have to specify the compute buffer format here since we need uvec4 29 | // not needed for the rest, the default format for vertex/index buffers is vec4/uint 30 | lightGridBuffer = 31 | bgfx::createDynamicIndexBuffer(CLUSTER_COUNT * 4, 32 | BGFX_BUFFER_COMPUTE_READ_WRITE | BGFX_BUFFER_INDEX32 | 33 | BGFX_BUFFER_COMPUTE_FORMAT_32X4 | BGFX_BUFFER_COMPUTE_TYPE_UINT); 34 | atomicIndexBuffer = bgfx::createDynamicIndexBuffer(1, BGFX_BUFFER_COMPUTE_READ_WRITE | BGFX_BUFFER_INDEX32); 35 | } 36 | 37 | void ClusterShader::shutdown() 38 | { 39 | bgfx::destroy(clusterSizesVecUniform); 40 | bgfx::destroy(zNearFarVecUniform); 41 | 42 | bgfx::destroy(clustersBuffer); 43 | bgfx::destroy(lightIndicesBuffer); 44 | bgfx::destroy(lightGridBuffer); 45 | bgfx::destroy(atomicIndexBuffer); 46 | 47 | clusterSizesVecUniform = zNearFarVecUniform = BGFX_INVALID_HANDLE; 48 | clustersBuffer = BGFX_INVALID_HANDLE; 49 | lightIndicesBuffer = lightGridBuffer = atomicIndexBuffer = BGFX_INVALID_HANDLE; 50 | } 51 | 52 | void ClusterShader::setUniforms(const Scene* scene, uint16_t screenWidth, uint16_t screenHeight) const 53 | { 54 | assert(scene != nullptr); 55 | 56 | float clusterSizesVec[4] = { std::ceil((float)screenWidth / CLUSTERS_X), 57 | std::ceil((float)screenHeight / CLUSTERS_Y) }; 58 | 59 | bgfx::setUniform(clusterSizesVecUniform, clusterSizesVec); 60 | float zNearFarVec[4] = { scene->camera.zNear, scene->camera.zFar }; 61 | bgfx::setUniform(zNearFarVecUniform, zNearFarVec); 62 | } 63 | 64 | void ClusterShader::bindBuffers(bool lightingPass) const 65 | { 66 | // binding ReadWrite in the fragment shader doesn't work with D3D11/12 67 | bgfx::Access::Enum access = lightingPass ? bgfx::Access::Read : bgfx::Access::ReadWrite; 68 | if(!lightingPass) 69 | { 70 | bgfx::setBuffer(Samplers::CLUSTERS_CLUSTERS, clustersBuffer, access); 71 | bgfx::setBuffer(Samplers::CLUSTERS_ATOMICINDEX, atomicIndexBuffer, access); 72 | } 73 | bgfx::setBuffer(Samplers::CLUSTERS_LIGHTINDICES, lightIndicesBuffer, access); 74 | bgfx::setBuffer(Samplers::CLUSTERS_LIGHTGRID, lightGridBuffer, access); 75 | } 76 | -------------------------------------------------------------------------------- /src/Renderer/ClusterShader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class Scene; 6 | 7 | class ClusterShader 8 | { 9 | public: 10 | ClusterShader(); 11 | 12 | void initialize(); 13 | void shutdown(); 14 | 15 | void setUniforms(const Scene* scene, uint16_t screenWidth, uint16_t screenHeight) const; 16 | void bindBuffers(bool lightingPass = true) const; 17 | 18 | static constexpr uint32_t CLUSTERS_X = 16; 19 | static constexpr uint32_t CLUSTERS_Y = 8; 20 | static constexpr uint32_t CLUSTERS_Z = 24; 21 | 22 | // limit number of threads (D3D only allows up to 1024, there might also be shared memory limitations) 23 | // shader will be run by 6 work groups 24 | static constexpr uint32_t CLUSTERS_X_THREADS = 16; 25 | static constexpr uint32_t CLUSTERS_Y_THREADS = 8; 26 | static constexpr uint32_t CLUSTERS_Z_THREADS = 4; 27 | 28 | static constexpr uint32_t CLUSTER_COUNT = CLUSTERS_X * CLUSTERS_Y * CLUSTERS_Z; 29 | 30 | static constexpr uint32_t MAX_LIGHTS_PER_CLUSTER = 100; 31 | 32 | private: 33 | struct ClusterVertex 34 | { 35 | // w is padding 36 | float minBounds[4]; 37 | float maxBounds[4]; 38 | 39 | static void init() 40 | { 41 | layout.begin() 42 | .add(bgfx::Attrib::TexCoord0, 4, bgfx::AttribType::Float) 43 | .add(bgfx::Attrib::TexCoord1, 4, bgfx::AttribType::Float) 44 | .end(); 45 | } 46 | static bgfx::VertexLayout layout; 47 | }; 48 | 49 | bgfx::UniformHandle clusterSizesVecUniform = BGFX_INVALID_HANDLE; 50 | bgfx::UniformHandle zNearFarVecUniform = BGFX_INVALID_HANDLE; 51 | 52 | // dynamic buffers can be created empty 53 | bgfx::DynamicVertexBufferHandle clustersBuffer = BGFX_INVALID_HANDLE; 54 | bgfx::DynamicIndexBufferHandle lightIndicesBuffer = BGFX_INVALID_HANDLE; 55 | bgfx::DynamicIndexBufferHandle lightGridBuffer = BGFX_INVALID_HANDLE; 56 | bgfx::DynamicIndexBufferHandle atomicIndexBuffer = BGFX_INVALID_HANDLE; 57 | }; 58 | -------------------------------------------------------------------------------- /src/Renderer/ClusteredRenderer.cpp: -------------------------------------------------------------------------------- 1 | #include "ClusteredRenderer.h" 2 | 3 | #include "Scene/Scene.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | ClusteredRenderer::ClusteredRenderer(const Scene* scene) : Renderer(scene) { } 10 | 11 | bool ClusteredRenderer::supported() 12 | { 13 | const bgfx::Caps* caps = bgfx::getCaps(); 14 | return Renderer::supported() && 15 | // compute shader 16 | (caps->supported & BGFX_CAPS_COMPUTE) != 0 && 17 | // 32-bit index buffers, used for light grid structure 18 | (caps->supported & BGFX_CAPS_INDEX32) != 0; 19 | } 20 | 21 | void ClusteredRenderer::onInitialize() 22 | { 23 | // OpenGL backend: uniforms must be created before loading shaders 24 | clusters.initialize(); 25 | 26 | char csName[128], vsName[128], fsName[128]; 27 | 28 | bx::snprintf(csName, BX_COUNTOF(csName), "%s%s", shaderDir(), "cs_clustered_clusterbuilding.bin"); 29 | clusterBuildingComputeProgram = bgfx::createProgram(bigg::loadShader(csName), true); 30 | 31 | bx::snprintf(csName, BX_COUNTOF(csName), "%s%s", shaderDir(), "cs_clustered_reset_counter.bin"); 32 | resetCounterComputeProgram = bgfx::createProgram(bigg::loadShader(csName), true); 33 | 34 | bx::snprintf(csName, BX_COUNTOF(csName), "%s%s", shaderDir(), "cs_clustered_lightculling.bin"); 35 | lightCullingComputeProgram = bgfx::createProgram(bigg::loadShader(csName), true); 36 | 37 | bx::snprintf(vsName, BX_COUNTOF(vsName), "%s%s", shaderDir(), "vs_clustered.bin"); 38 | bx::snprintf(fsName, BX_COUNTOF(fsName), "%s%s", shaderDir(), "fs_clustered.bin"); 39 | lightingProgram = bigg::loadProgram(vsName, fsName); 40 | 41 | bx::snprintf(fsName, BX_COUNTOF(fsName), "%s%s", shaderDir(), "fs_clustered_debug_vis.bin"); 42 | debugVisProgram = bigg::loadProgram(vsName, fsName); 43 | } 44 | 45 | void ClusteredRenderer::onRender(float dt) 46 | { 47 | enum : bgfx::ViewId 48 | { 49 | vClusterBuilding = 0, 50 | vLightCulling, 51 | vLighting 52 | }; 53 | 54 | bgfx::setViewName(vClusterBuilding, "Cluster building pass (compute)"); 55 | // set u_viewRect for screen2Eye to work correctly 56 | bgfx::setViewRect(vClusterBuilding, 0, 0, width, height); 57 | 58 | bgfx::setViewName(vLightCulling, "Clustered light culling pass (compute)"); 59 | bgfx::setViewRect(vLightCulling, 0, 0, width, height); 60 | 61 | bgfx::setViewName(vLighting, "Clustered lighting pass"); 62 | bgfx::setViewClear(vLighting, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, clearColor, 1.0f, 0); 63 | bgfx::setViewRect(vLighting, 0, 0, width, height); 64 | bgfx::setViewFrameBuffer(vLighting, frameBuffer); 65 | bgfx::touch(vLighting); 66 | 67 | if(!scene->loaded) 68 | return; 69 | 70 | clusters.setUniforms(scene, width, height); 71 | 72 | // cluster building needs u_invProj to transform screen coordinates to eye space 73 | setViewProjection(vClusterBuilding); 74 | // light culling needs u_view to transform lights to eye space 75 | setViewProjection(vLightCulling); 76 | setViewProjection(vLighting); 77 | 78 | // cluster building 79 | 80 | // only run this step if the camera parameters changed (aspect ratio, fov, near/far plane) 81 | // cluster bounds are saved in camera coordinates so they don't change with camera movement 82 | 83 | // ideally we'd compare the relative error here but a correct implementation would involve 84 | // a bunch of costly matrix operations: https://floating-point-gui.de/errors/comparison/ 85 | // comparing the absolute error against a rather small epsilon here works as long as the values 86 | // in the projection matrix aren't getting too large 87 | bool buildClusters = glm::any(glm::notEqual(projMat, oldProjMat, 0.00001f)); 88 | if(buildClusters) 89 | { 90 | oldProjMat = projMat; 91 | 92 | clusters.bindBuffers(false /*lightingPass*/); // write access, all buffers 93 | 94 | bgfx::dispatch(vClusterBuilding, 95 | clusterBuildingComputeProgram, 96 | ClusterShader::CLUSTERS_X / ClusterShader::CLUSTERS_X_THREADS, 97 | ClusterShader::CLUSTERS_Y / ClusterShader::CLUSTERS_Y_THREADS, 98 | ClusterShader::CLUSTERS_Z / ClusterShader::CLUSTERS_Z_THREADS); 99 | } 100 | 101 | // light culling 102 | 103 | clusters.bindBuffers(false); 104 | 105 | // reset atomic counter for light grid generation 106 | // buffers created with BGFX_BUFFER_COMPUTE_WRITE can't be updated from the CPU 107 | // this used to happen during cluster building when it was still run every frame 108 | bgfx::dispatch(vLightCulling, resetCounterComputeProgram, 1, 1, 1); 109 | 110 | lights.bindLights(scene); 111 | clusters.bindBuffers(false); 112 | 113 | bgfx::dispatch(vLightCulling, 114 | lightCullingComputeProgram, 115 | ClusterShader::CLUSTERS_X / ClusterShader::CLUSTERS_X_THREADS, 116 | ClusterShader::CLUSTERS_Y / ClusterShader::CLUSTERS_Y_THREADS, 117 | ClusterShader::CLUSTERS_Z / ClusterShader::CLUSTERS_Z_THREADS); 118 | // lighting 119 | 120 | bool debugVis = variables["DEBUG_VIS"] == "true"; 121 | bgfx::ProgramHandle program = debugVis ? debugVisProgram : lightingProgram; 122 | 123 | uint64_t state = BGFX_STATE_DEFAULT & ~BGFX_STATE_CULL_MASK; 124 | 125 | pbr.bindAlbedoLUT(); 126 | lights.bindLights(scene); 127 | clusters.bindBuffers(true /*lightingPass*/); // read access, only light grid and indices 128 | 129 | for(const Mesh& mesh : scene->meshes) 130 | { 131 | glm::mat4 model = glm::identity(); 132 | bgfx::setTransform(glm::value_ptr(model)); 133 | setNormalMatrix(model); 134 | bgfx::setVertexBuffer(0, mesh.vertexBuffer); 135 | bgfx::setIndexBuffer(mesh.indexBuffer); 136 | const Material& mat = scene->materials[mesh.material]; 137 | uint64_t materialState = pbr.bindMaterial(mat); 138 | bgfx::setState(state | materialState); 139 | // preserve buffer bindings between submit calls 140 | bgfx::submit(vLighting, program, 0, ~BGFX_DISCARD_BINDINGS); 141 | } 142 | 143 | bgfx::discard(BGFX_DISCARD_ALL); 144 | } 145 | 146 | void ClusteredRenderer::onShutdown() 147 | { 148 | clusters.shutdown(); 149 | 150 | bgfx::destroy(clusterBuildingComputeProgram); 151 | bgfx::destroy(resetCounterComputeProgram); 152 | bgfx::destroy(lightCullingComputeProgram); 153 | bgfx::destroy(lightingProgram); 154 | bgfx::destroy(debugVisProgram); 155 | 156 | clusterBuildingComputeProgram = resetCounterComputeProgram = lightCullingComputeProgram = lightingProgram = 157 | debugVisProgram = BGFX_INVALID_HANDLE; 158 | } 159 | -------------------------------------------------------------------------------- /src/Renderer/ClusteredRenderer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Renderer.h" 4 | #include "ClusterShader.h" 5 | 6 | class ClusteredRenderer : public Renderer 7 | { 8 | public: 9 | ClusteredRenderer(const Scene* scene); 10 | 11 | static bool supported(); 12 | 13 | virtual void onInitialize() override; 14 | virtual void onRender(float dt) override; 15 | virtual void onShutdown() override; 16 | 17 | private: 18 | glm::mat4 oldProjMat = glm::mat4(0.0f); 19 | 20 | bgfx::ProgramHandle clusterBuildingComputeProgram = BGFX_INVALID_HANDLE; 21 | bgfx::ProgramHandle resetCounterComputeProgram = BGFX_INVALID_HANDLE; 22 | bgfx::ProgramHandle lightCullingComputeProgram = BGFX_INVALID_HANDLE; 23 | bgfx::ProgramHandle lightingProgram = BGFX_INVALID_HANDLE; 24 | bgfx::ProgramHandle debugVisProgram = BGFX_INVALID_HANDLE; 25 | 26 | ClusterShader clusters; 27 | }; 28 | -------------------------------------------------------------------------------- /src/Renderer/DeferredRenderer.cpp: -------------------------------------------------------------------------------- 1 | #include "DeferredRenderer.h" 2 | 3 | #include "Scene/Scene.h" 4 | #include "Renderer/Samplers.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | constexpr bgfx::TextureFormat::Enum 11 | DeferredRenderer::gBufferAttachmentFormats[DeferredRenderer::GBufferAttachment::Count - 1]; 12 | 13 | DeferredRenderer::DeferredRenderer(const Scene* scene) : 14 | Renderer(scene), 15 | gBufferTextures { { BGFX_INVALID_HANDLE, "Diffuse + roughness" }, 16 | { BGFX_INVALID_HANDLE, "Normal" }, 17 | { BGFX_INVALID_HANDLE, "F0 + metallic" }, 18 | { BGFX_INVALID_HANDLE, "Emissive + occlusion" }, 19 | { BGFX_INVALID_HANDLE, "Depth" }, 20 | { BGFX_INVALID_HANDLE, nullptr } }, 21 | gBufferTextureUnits { Samplers::DEFERRED_DIFFUSE_A, 22 | Samplers::DEFERRED_NORMAL, 23 | Samplers::DEFERRED_F0_METALLIC, 24 | Samplers::DEFERRED_EMISSIVE_OCCLUSION, 25 | Samplers::DEFERRED_DEPTH }, 26 | gBufferSamplerNames { "s_texDiffuseA", "s_texNormal", "s_texF0Metallic", "s_texEmissiveOcclusion", "s_texDepth" } 27 | { 28 | for(bgfx::UniformHandle& handle : gBufferSamplers) 29 | { 30 | handle = BGFX_INVALID_HANDLE; 31 | } 32 | buffers = gBufferTextures; 33 | } 34 | 35 | bool DeferredRenderer::supported() 36 | { 37 | const bgfx::Caps* caps = bgfx::getCaps(); 38 | bool supported = Renderer::supported() && 39 | // blitting depth texture after geometry pass 40 | (caps->supported & BGFX_CAPS_TEXTURE_BLIT) != 0 && 41 | // multiple render targets 42 | // depth doesn't count as an attachment 43 | caps->limits.maxFBAttachments >= GBufferAttachment::Count - 1; 44 | if(!supported) 45 | return false; 46 | 47 | for(bgfx::TextureFormat::Enum format : gBufferAttachmentFormats) 48 | { 49 | if((caps->formats[format] & BGFX_CAPS_FORMAT_TEXTURE_FRAMEBUFFER) == 0) 50 | return false; 51 | } 52 | 53 | return true; 54 | } 55 | 56 | void DeferredRenderer::onInitialize() 57 | { 58 | for(size_t i = 0; i < BX_COUNTOF(gBufferSamplers); i++) 59 | { 60 | gBufferSamplers[i] = bgfx::createUniform(gBufferSamplerNames[i], bgfx::UniformType::Sampler); 61 | } 62 | lightIndexVecUniform = bgfx::createUniform("u_lightIndexVec", bgfx::UniformType::Vec4); 63 | 64 | // axis-aligned bounding box used as light geometry for light culling 65 | constexpr float LEFT = -1.0f, RIGHT = 1.0f, BOTTOM = -1.0f, TOP = 1.0f, FRONT = -1.0f, BACK = 1.0f; 66 | const PosVertex vertices[8] = { 67 | { LEFT, BOTTOM, FRONT }, { RIGHT, BOTTOM, FRONT }, { LEFT, TOP, FRONT }, { RIGHT, TOP, FRONT }, 68 | { LEFT, BOTTOM, BACK }, { RIGHT, BOTTOM, BACK }, { LEFT, TOP, BACK }, { RIGHT, TOP, BACK }, 69 | }; 70 | const uint16_t indices[6 * 6] = { 71 | // CCW 72 | 0, 1, 3, 3, 2, 0, // front 73 | 5, 4, 6, 6, 7, 5, // back 74 | 4, 0, 2, 2, 6, 4, // left 75 | 1, 5, 7, 7, 3, 1, // right 76 | 2, 3, 7, 7, 6, 2, // top 77 | 4, 5, 1, 1, 0, 4 // bottom 78 | }; 79 | 80 | pointLightVertexBuffer = bgfx::createVertexBuffer(bgfx::copy(&vertices, sizeof(vertices)), PosVertex::layout); 81 | pointLightIndexBuffer = bgfx::createIndexBuffer(bgfx::copy(&indices, sizeof(indices))); 82 | 83 | char vsName[128], fsName[128]; 84 | 85 | bx::snprintf(vsName, BX_COUNTOF(vsName), "%s%s", shaderDir(), "vs_deferred_geometry.bin"); 86 | bx::snprintf(fsName, BX_COUNTOF(fsName), "%s%s", shaderDir(), "fs_deferred_geometry.bin"); 87 | geometryProgram = bigg::loadProgram(vsName, fsName); 88 | 89 | bx::snprintf(vsName, BX_COUNTOF(vsName), "%s%s", shaderDir(), "vs_deferred_fullscreen.bin"); 90 | bx::snprintf(fsName, BX_COUNTOF(fsName), "%s%s", shaderDir(), "fs_deferred_fullscreen.bin"); 91 | fullscreenProgram = bigg::loadProgram(vsName, fsName); 92 | 93 | bx::snprintf(vsName, BX_COUNTOF(vsName), "%s%s", shaderDir(), "vs_deferred_light.bin"); 94 | bx::snprintf(fsName, BX_COUNTOF(fsName), "%s%s", shaderDir(), "fs_deferred_pointlight.bin"); 95 | pointLightProgram = bigg::loadProgram(vsName, fsName); 96 | 97 | bx::snprintf(vsName, BX_COUNTOF(vsName), "%s%s", shaderDir(), "vs_forward.bin"); 98 | bx::snprintf(fsName, BX_COUNTOF(fsName), "%s%s", shaderDir(), "fs_forward.bin"); 99 | transparencyProgram = bigg::loadProgram(vsName, fsName); 100 | } 101 | 102 | void DeferredRenderer::onReset() 103 | { 104 | if(!bgfx::isValid(gBuffer)) 105 | { 106 | gBuffer = createGBuffer(); 107 | 108 | for(size_t i = 0; i < GBufferAttachment::Depth; i++) 109 | { 110 | gBufferTextures[i].handle = bgfx::getTexture(gBuffer, (uint8_t)i); 111 | } 112 | 113 | // we can't use the G-Buffer's depth texture in the light pass framebuffer 114 | // binding a texture for reading in the shader and attaching it to a framebuffer 115 | // at the same time is undefined behaviour in most APIs 116 | // https://www.khronos.org/opengl/wiki/Memory_Model#Framebuffer_objects 117 | // we use a different depth texture and just blit it between the geometry and light pass 118 | const uint64_t flags = BGFX_TEXTURE_BLIT_DST | gBufferSamplerFlags; 119 | bgfx::TextureFormat::Enum depthFormat = findDepthFormat(flags); 120 | lightDepthTexture = bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, depthFormat, flags); 121 | 122 | gBufferTextures[GBufferAttachment::Depth].handle = lightDepthTexture; 123 | } 124 | 125 | if(!bgfx::isValid(accumFrameBuffer)) 126 | { 127 | const bgfx::TextureHandle textures[2] = { bgfx::getTexture(frameBuffer, 0), 128 | bgfx::getTexture(gBuffer, GBufferAttachment::Depth) }; 129 | accumFrameBuffer = bgfx::createFrameBuffer(BX_COUNTOF(textures), textures); // don't destroy textures 130 | } 131 | } 132 | 133 | void DeferredRenderer::onRender(float dt) 134 | { 135 | enum : bgfx::ViewId 136 | { 137 | vGeometry = 0, // write G-Buffer 138 | vFullscreenLight, // write ambient + emissive to output buffer 139 | vLight, // render lights to output buffer 140 | vTransparent // forward pass for transparency 141 | }; 142 | 143 | const uint32_t BLACK = 0x000000FF; 144 | 145 | bgfx::setViewName(vGeometry, "Deferred geometry pass"); 146 | bgfx::setViewClear(vGeometry, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, BLACK, 1.0f); 147 | bgfx::setViewRect(vGeometry, 0, 0, width, height); 148 | bgfx::setViewFrameBuffer(vGeometry, gBuffer); 149 | bgfx::touch(vGeometry); 150 | 151 | bgfx::setViewName(vFullscreenLight, "Deferred light pass (ambient + emissive)"); 152 | bgfx::setViewClear(vFullscreenLight, BGFX_CLEAR_COLOR, clearColor); 153 | bgfx::setViewRect(vFullscreenLight, 0, 0, width, height); 154 | bgfx::setViewFrameBuffer(vFullscreenLight, accumFrameBuffer); 155 | bgfx::touch(vFullscreenLight); 156 | 157 | bgfx::setViewName(vLight, "Deferred light pass (point lights)"); 158 | bgfx::setViewClear(vLight, BGFX_CLEAR_NONE); 159 | bgfx::setViewRect(vLight, 0, 0, width, height); 160 | bgfx::setViewFrameBuffer(vLight, accumFrameBuffer); 161 | bgfx::touch(vLight); 162 | 163 | bgfx::setViewName(vTransparent, "Transparent forward pass"); 164 | bgfx::setViewClear(vTransparent, BGFX_CLEAR_NONE); 165 | bgfx::setViewRect(vTransparent, 0, 0, width, height); 166 | bgfx::setViewFrameBuffer(vTransparent, accumFrameBuffer); 167 | bgfx::touch(vTransparent); 168 | 169 | if(!scene->loaded) 170 | return; 171 | 172 | setViewProjection(vGeometry); 173 | setViewProjection(vFullscreenLight); 174 | setViewProjection(vLight); 175 | setViewProjection(vTransparent); 176 | 177 | // render geometry, write to G-Buffer 178 | 179 | const uint64_t state = BGFX_STATE_DEFAULT & ~BGFX_STATE_CULL_MASK; 180 | 181 | for(const Mesh& mesh : scene->meshes) 182 | { 183 | const Material& mat = scene->materials[mesh.material]; 184 | // transparent materials are rendered in a separate forward pass (view vTransparent) 185 | if(!mat.blend) 186 | { 187 | glm::mat4 model = glm::identity(); 188 | bgfx::setTransform(glm::value_ptr(model)); 189 | setNormalMatrix(model); 190 | bgfx::setVertexBuffer(0, mesh.vertexBuffer); 191 | bgfx::setIndexBuffer(mesh.indexBuffer); 192 | uint64_t materialState = pbr.bindMaterial(mat); 193 | bgfx::setState(state | materialState); 194 | bgfx::submit(vGeometry, geometryProgram); 195 | } 196 | } 197 | 198 | // copy G-Buffer depth attachment to depth texture for sampling in the light pass 199 | // we can't attach it to the frame buffer and read it in the shader (unprojecting world position) at the same time 200 | // blit happens before any compute or draw calls 201 | bgfx::blit(vFullscreenLight, lightDepthTexture, 0, 0, bgfx::getTexture(gBuffer, GBufferAttachment::Depth)); 202 | 203 | // bind these once for all following submits 204 | // excluding BGFX_DISCARD_TEXTURE_SAMPLERS from the discard flags passed to submit makes sure 205 | // they don't get unbound 206 | bindGBuffer(); 207 | pbr.bindAlbedoLUT(); 208 | lights.bindLights(scene); 209 | 210 | // ambient light + emissive 211 | 212 | // full screen triangle, moved to far plane in the shader 213 | // only render if the geometry is in front so we leave the background untouched 214 | bgfx::setVertexBuffer(0, blitTriangleBuffer); 215 | bgfx::setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_DEPTH_TEST_GREATER | BGFX_STATE_CULL_CW); 216 | bgfx::submit(vFullscreenLight, fullscreenProgram, 0, ~BGFX_DISCARD_BINDINGS); 217 | 218 | // point lights 219 | 220 | // render lights to framebuffer 221 | // cull with light geometry 222 | // - axis-aligned bounding box (TODO? sphere for point lights) 223 | // - read depth from geometry pass 224 | // - reverse depth test 225 | // - render backfaces 226 | // - this shades all pixels between camera and backfaces 227 | // accumulate light contributions (blend mode add) 228 | // TODO? tiled-deferred is probably faster for small lights 229 | // https://software.intel.com/sites/default/files/m/d/4/1/d/8/lauritzen_deferred_shading_siggraph_2010.pdf 230 | 231 | bgfx::setVertexBuffer(0, pointLightVertexBuffer); 232 | bgfx::setIndexBuffer(pointLightIndexBuffer); 233 | 234 | for(size_t i = 0; i < scene->pointLights.lights.size(); i++) 235 | { 236 | // position light geometry (bounding box) 237 | // TODO if the light extends past the far plane, it won't get rendered 238 | // - clip light extents to not extend past far plane 239 | // - use screen aligned quads (how to test depth?) 240 | // - tiled-deferred 241 | const PointLight& light = scene->pointLights.lights[i]; 242 | float radius = light.calculateRadius(); 243 | glm::mat4 scale = glm::scale(glm::identity(), glm::vec3(radius)); 244 | glm::mat4 translate = glm::translate(glm::identity(), light.position); 245 | glm::mat4 model = translate * scale; 246 | bgfx::setTransform(glm::value_ptr(model)); 247 | float lightIndexVec[4] = { (float)i }; 248 | bgfx::setUniform(lightIndexVecUniform, lightIndexVec); 249 | bgfx::setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_DEPTH_TEST_GEQUAL | BGFX_STATE_CULL_CCW | 250 | BGFX_STATE_BLEND_ADD); 251 | bgfx::submit(vLight, 252 | pointLightProgram, 253 | 0, 254 | ~(BGFX_DISCARD_VERTEX_STREAMS | BGFX_DISCARD_INDEX_BUFFER | BGFX_DISCARD_BINDINGS)); 255 | } 256 | 257 | // transparent 258 | 259 | for(const Mesh& mesh : scene->meshes) 260 | { 261 | const Material& mat = scene->materials[mesh.material]; 262 | if(mat.blend) 263 | { 264 | glm::mat4 model = glm::identity(); 265 | bgfx::setTransform(glm::value_ptr(model)); 266 | setNormalMatrix(model); 267 | bgfx::setVertexBuffer(0, mesh.vertexBuffer); 268 | bgfx::setIndexBuffer(mesh.indexBuffer); 269 | uint64_t materialState = pbr.bindMaterial(mat); 270 | bgfx::setState(state | materialState); 271 | bgfx::submit(vTransparent, transparencyProgram, 0, ~BGFX_DISCARD_BINDINGS); 272 | } 273 | } 274 | 275 | bgfx::discard(BGFX_DISCARD_ALL); 276 | } 277 | 278 | void DeferredRenderer::onShutdown() 279 | { 280 | bgfx::destroy(geometryProgram); 281 | bgfx::destroy(pointLightProgram); 282 | bgfx::destroy(fullscreenProgram); 283 | bgfx::destroy(transparencyProgram); 284 | for(bgfx::UniformHandle& handle : gBufferSamplers) 285 | { 286 | bgfx::destroy(handle); 287 | handle = BGFX_INVALID_HANDLE; 288 | } 289 | bgfx::destroy(pointLightVertexBuffer); 290 | bgfx::destroy(pointLightIndexBuffer); 291 | if(bgfx::isValid(lightDepthTexture)) 292 | bgfx::destroy(lightDepthTexture); 293 | if(bgfx::isValid(gBuffer)) 294 | bgfx::destroy(gBuffer); 295 | if(bgfx::isValid(accumFrameBuffer)) 296 | bgfx::destroy(accumFrameBuffer); 297 | 298 | geometryProgram = fullscreenProgram = pointLightProgram = transparencyProgram = BGFX_INVALID_HANDLE; 299 | lightIndexVecUniform = BGFX_INVALID_HANDLE; 300 | pointLightVertexBuffer = BGFX_INVALID_HANDLE; 301 | pointLightIndexBuffer = BGFX_INVALID_HANDLE; 302 | lightDepthTexture = BGFX_INVALID_HANDLE; 303 | gBuffer = BGFX_INVALID_HANDLE; 304 | accumFrameBuffer = BGFX_INVALID_HANDLE; 305 | } 306 | 307 | bgfx::FrameBufferHandle DeferredRenderer::createGBuffer() 308 | { 309 | bgfx::TextureHandle textures[GBufferAttachment::Count]; 310 | 311 | const uint64_t flags = BGFX_TEXTURE_RT | gBufferSamplerFlags; 312 | 313 | for(size_t i = 0; i < GBufferAttachment::Depth; i++) 314 | { 315 | assert(bgfx::isTextureValid(0, false, 1, gBufferAttachmentFormats[i], flags)); 316 | textures[i] = bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, gBufferAttachmentFormats[i], flags); 317 | } 318 | 319 | bgfx::TextureFormat::Enum depthFormat = findDepthFormat(flags); 320 | assert(depthFormat != bgfx::TextureFormat::Count); 321 | textures[Depth] = bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, depthFormat, flags); 322 | 323 | bgfx::FrameBufferHandle gb = bgfx::createFrameBuffer((uint8_t)GBufferAttachment::Count, textures, true); 324 | 325 | if(!bgfx::isValid(gb)) 326 | Log->error("Failed to create G-Buffer"); 327 | else 328 | bgfx::setName(gb, "G-Buffer"); 329 | 330 | return gb; 331 | } 332 | 333 | void DeferredRenderer::bindGBuffer() 334 | { 335 | for(size_t i = 0; i < GBufferAttachment::Count; i++) 336 | { 337 | bgfx::setTexture(gBufferTextureUnits[i], gBufferSamplers[i], gBufferTextures[i].handle); 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /src/Renderer/DeferredRenderer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Renderer.h" 4 | 5 | class DeferredRenderer : public Renderer 6 | { 7 | public: 8 | DeferredRenderer(const Scene* scene); 9 | 10 | static bool supported(); 11 | 12 | virtual void onInitialize() override; 13 | virtual void onReset() override; 14 | virtual void onRender(float dt) override; 15 | virtual void onShutdown() override; 16 | 17 | private: 18 | bgfx::VertexBufferHandle pointLightVertexBuffer = BGFX_INVALID_HANDLE; 19 | bgfx::IndexBufferHandle pointLightIndexBuffer = BGFX_INVALID_HANDLE; 20 | 21 | enum GBufferAttachment : size_t 22 | { 23 | // no world position 24 | // gl_Fragcoord is enough to unproject 25 | 26 | // RGB = diffuse 27 | // A = a (remapped roughness) 28 | Diffuse_A, 29 | 30 | // RG = encoded normal 31 | Normal, 32 | 33 | // RGB = F0 (Fresnel at normal incidence) 34 | // A = metallic 35 | // TODO? don't use F0, calculate from diffuse and metallic in shader 36 | // where do we store metallic? 37 | F0_Metallic, 38 | 39 | // RGB = emissive radiance 40 | // A = occlusion multiplier 41 | EmissiveOcclusion, 42 | 43 | Depth, 44 | 45 | Count 46 | }; 47 | 48 | static constexpr uint64_t gBufferSamplerFlags = BGFX_SAMPLER_MIN_POINT | BGFX_SAMPLER_MAG_POINT | 49 | BGFX_SAMPLER_MIP_POINT | BGFX_SAMPLER_U_CLAMP | 50 | BGFX_SAMPLER_V_CLAMP; 51 | 52 | static constexpr bgfx::TextureFormat::Enum gBufferAttachmentFormats[GBufferAttachment::Count - 1] = { 53 | bgfx::TextureFormat::BGRA8, 54 | bgfx::TextureFormat::RG16F, 55 | bgfx::TextureFormat::BGRA8, 56 | bgfx::TextureFormat::BGRA8 57 | // depth format is determined dynamically 58 | }; 59 | 60 | TextureBuffer gBufferTextures[GBufferAttachment::Count + 1]; // includes depth, + null-terminated 61 | uint8_t gBufferTextureUnits[GBufferAttachment::Count]; 62 | const char* gBufferSamplerNames[GBufferAttachment::Count]; 63 | bgfx::UniformHandle gBufferSamplers[GBufferAttachment::Count]; 64 | bgfx::FrameBufferHandle gBuffer = BGFX_INVALID_HANDLE; 65 | 66 | bgfx::TextureHandle lightDepthTexture = BGFX_INVALID_HANDLE; 67 | bgfx::FrameBufferHandle accumFrameBuffer = BGFX_INVALID_HANDLE; 68 | 69 | bgfx::UniformHandle lightIndexVecUniform = BGFX_INVALID_HANDLE; 70 | 71 | bgfx::ProgramHandle geometryProgram = BGFX_INVALID_HANDLE; 72 | bgfx::ProgramHandle fullscreenProgram = BGFX_INVALID_HANDLE; 73 | bgfx::ProgramHandle pointLightProgram = BGFX_INVALID_HANDLE; 74 | bgfx::ProgramHandle transparencyProgram = BGFX_INVALID_HANDLE; 75 | 76 | static bgfx::FrameBufferHandle createGBuffer(); 77 | void bindGBuffer(); 78 | }; 79 | -------------------------------------------------------------------------------- /src/Renderer/ForwardRenderer.cpp: -------------------------------------------------------------------------------- 1 | #include "ForwardRenderer.h" 2 | 3 | #include "Scene/Scene.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | ForwardRenderer::ForwardRenderer(const Scene* scene) : Renderer(scene) { } 10 | 11 | bool ForwardRenderer::supported() 12 | { 13 | return Renderer::supported(); 14 | } 15 | 16 | void ForwardRenderer::onInitialize() 17 | { 18 | char vsName[128], fsName[128]; 19 | bx::snprintf(vsName, BX_COUNTOF(vsName), "%s%s", shaderDir(), "vs_forward.bin"); 20 | bx::snprintf(fsName, BX_COUNTOF(fsName), "%s%s", shaderDir(), "fs_forward.bin"); 21 | program = bigg::loadProgram(vsName, fsName); 22 | } 23 | 24 | void ForwardRenderer::onRender(float dt) 25 | { 26 | bgfx::ViewId vDefault = 0; 27 | 28 | bgfx::setViewName(vDefault, "Forward render pass"); 29 | bgfx::setViewClear(vDefault, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, clearColor, 1.0f, 0); 30 | bgfx::setViewRect(vDefault, 0, 0, width, height); 31 | bgfx::setViewFrameBuffer(vDefault, frameBuffer); 32 | 33 | // empty primitive in case nothing follows 34 | // this makes sure the clear happens 35 | bgfx::touch(vDefault); 36 | 37 | if(!scene->loaded) 38 | return; 39 | 40 | setViewProjection(vDefault); 41 | 42 | uint64_t state = BGFX_STATE_DEFAULT & ~BGFX_STATE_CULL_MASK; 43 | 44 | pbr.bindAlbedoLUT(); 45 | lights.bindLights(scene); 46 | 47 | for(const Mesh& mesh : scene->meshes) 48 | { 49 | glm::mat4 model = glm::identity(); 50 | bgfx::setTransform(glm::value_ptr(model)); 51 | setNormalMatrix(model); 52 | bgfx::setVertexBuffer(0, mesh.vertexBuffer); 53 | bgfx::setIndexBuffer(mesh.indexBuffer); 54 | const Material& mat = scene->materials[mesh.material]; 55 | uint64_t materialState = pbr.bindMaterial(mat); 56 | bgfx::setState(state | materialState); 57 | bgfx::submit(vDefault, program, 0, ~BGFX_DISCARD_BINDINGS); 58 | } 59 | 60 | bgfx::discard(BGFX_DISCARD_ALL); 61 | } 62 | 63 | void ForwardRenderer::onShutdown() 64 | { 65 | bgfx::destroy(program); 66 | program = BGFX_INVALID_HANDLE; 67 | } 68 | -------------------------------------------------------------------------------- /src/Renderer/ForwardRenderer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Renderer.h" 4 | 5 | class ForwardRenderer : public Renderer 6 | { 7 | public: 8 | ForwardRenderer(const Scene* scene); 9 | 10 | static bool supported(); 11 | 12 | virtual void onInitialize() override; 13 | virtual void onRender(float dt) override; 14 | virtual void onShutdown() override; 15 | 16 | private: 17 | bgfx::ProgramHandle program = BGFX_INVALID_HANDLE; 18 | }; 19 | -------------------------------------------------------------------------------- /src/Renderer/LightShader.cpp: -------------------------------------------------------------------------------- 1 | #include "LightShader.h" 2 | 3 | #include "Scene/Scene.h" 4 | #include "Renderer/Samplers.h" 5 | #include 6 | #include 7 | 8 | void LightShader::initialize() 9 | { 10 | lightCountVecUniform = bgfx::createUniform("u_lightCountVec", bgfx::UniformType::Vec4); 11 | ambientLightIrradianceUniform = bgfx::createUniform("u_ambientLightIrradiance", bgfx::UniformType::Vec4); 12 | } 13 | 14 | void LightShader::shutdown() 15 | { 16 | bgfx::destroy(lightCountVecUniform); 17 | bgfx::destroy(ambientLightIrradianceUniform); 18 | 19 | lightCountVecUniform = ambientLightIrradianceUniform = BGFX_INVALID_HANDLE; 20 | } 21 | 22 | void LightShader::bindLights(const Scene* scene) const 23 | { 24 | assert(scene != nullptr); 25 | 26 | // a 32-bit IEEE 754 float can represent all integers up to 2^24 (~16.7 million) correctly 27 | // should be enough for this use case (comparison in for loop) 28 | float lightCountVec[4] = { (float)scene->pointLights.lights.size() }; 29 | bgfx::setUniform(lightCountVecUniform, lightCountVec); 30 | 31 | glm::vec4 ambientLightIrradiance(scene->ambientLight.irradiance, 1.0f); 32 | bgfx::setUniform(ambientLightIrradianceUniform, glm::value_ptr(ambientLightIrradiance)); 33 | 34 | bgfx::setBuffer(Samplers::LIGHTS_POINTLIGHTS, scene->pointLights.buffer, bgfx::Access::Read); 35 | } 36 | -------------------------------------------------------------------------------- /src/Renderer/LightShader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class Scene; 6 | 7 | class LightShader 8 | { 9 | public: 10 | void initialize(); 11 | void shutdown(); 12 | 13 | void bindLights(const Scene* scene) const; 14 | 15 | private: 16 | bgfx::UniformHandle lightCountVecUniform = BGFX_INVALID_HANDLE; 17 | bgfx::UniformHandle ambientLightIrradianceUniform = BGFX_INVALID_HANDLE; 18 | }; 19 | -------------------------------------------------------------------------------- /src/Renderer/PBRShader.cpp: -------------------------------------------------------------------------------- 1 | #include "PBRShader.h" 2 | 3 | #include "Scene/Material.h" 4 | #include "Renderer/Renderer.h" 5 | #include "Renderer/Samplers.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | void PBRShader::initialize() 13 | { 14 | baseColorFactorUniform = bgfx::createUniform("u_baseColorFactor", bgfx::UniformType::Vec4); 15 | metallicRoughnessNormalOcclusionFactorUniform = 16 | bgfx::createUniform("u_metallicRoughnessNormalOcclusionFactor", bgfx::UniformType::Vec4); 17 | emissiveFactorUniform = bgfx::createUniform("u_emissiveFactorVec", bgfx::UniformType::Vec4); 18 | hasTexturesUniform = bgfx::createUniform("u_hasTextures", bgfx::UniformType::Vec4); 19 | multipleScatteringUniform = bgfx::createUniform("u_multipleScatteringVec", bgfx::UniformType::Vec4); 20 | albedoLUTSampler = bgfx::createUniform("s_texAlbedoLUT", bgfx::UniformType::Sampler); 21 | baseColorSampler = bgfx::createUniform("s_texBaseColor", bgfx::UniformType::Sampler); 22 | metallicRoughnessSampler = bgfx::createUniform("s_texMetallicRoughness", bgfx::UniformType::Sampler); 23 | normalSampler = bgfx::createUniform("s_texNormal", bgfx::UniformType::Sampler); 24 | occlusionSampler = bgfx::createUniform("s_texOcclusion", bgfx::UniformType::Sampler); 25 | emissiveSampler = bgfx::createUniform("s_texEmissive", bgfx::UniformType::Sampler); 26 | 27 | defaultTexture = bgfx::createTexture2D(1, 1, false, 1, bgfx::TextureFormat::RGBA8); 28 | albedoLUTTexture = bgfx::createTexture2D(ALBEDO_LUT_SIZE, 29 | ALBEDO_LUT_SIZE, 30 | false, 31 | 1, 32 | bgfx::TextureFormat::RGBA32F, 33 | BGFX_SAMPLER_UVW_CLAMP | BGFX_TEXTURE_COMPUTE_WRITE); 34 | 35 | char csName[128]; 36 | bx::snprintf(csName, BX_COUNTOF(csName), "%s%s", Renderer::shaderDir(), "cs_multiple_scattering_lut.bin"); 37 | albedoLUTProgram = bgfx::createProgram(bigg::loadShader(csName), true); 38 | } 39 | 40 | void PBRShader::shutdown() 41 | { 42 | bgfx::destroy(baseColorFactorUniform); 43 | bgfx::destroy(metallicRoughnessNormalOcclusionFactorUniform); 44 | bgfx::destroy(emissiveFactorUniform); 45 | bgfx::destroy(hasTexturesUniform); 46 | bgfx::destroy(multipleScatteringUniform); 47 | bgfx::destroy(albedoLUTSampler); 48 | bgfx::destroy(baseColorSampler); 49 | bgfx::destroy(metallicRoughnessSampler); 50 | bgfx::destroy(normalSampler); 51 | bgfx::destroy(occlusionSampler); 52 | bgfx::destroy(emissiveSampler); 53 | bgfx::destroy(albedoLUTTexture); 54 | bgfx::destroy(defaultTexture); 55 | bgfx::destroy(albedoLUTProgram); 56 | 57 | baseColorFactorUniform = metallicRoughnessNormalOcclusionFactorUniform = emissiveFactorUniform = 58 | hasTexturesUniform = multipleScatteringUniform = albedoLUTSampler = baseColorSampler = 59 | metallicRoughnessSampler = normalSampler = occlusionSampler = emissiveSampler = BGFX_INVALID_HANDLE; 60 | albedoLUTTexture = defaultTexture = BGFX_INVALID_HANDLE; 61 | albedoLUTProgram = BGFX_INVALID_HANDLE; 62 | } 63 | 64 | void PBRShader::generateAlbedoLUT() 65 | { 66 | bindAlbedoLUT(true /* compute */); 67 | bgfx::dispatch(0, albedoLUTProgram, ALBEDO_LUT_SIZE / ALBEDO_LUT_THREADS, ALBEDO_LUT_SIZE / ALBEDO_LUT_THREADS, 1); 68 | } 69 | 70 | uint64_t PBRShader::bindMaterial(const Material& material) 71 | { 72 | float factorValues[4] = { 73 | material.metallicFactor, material.roughnessFactor, material.normalScale, material.occlusionStrength 74 | }; 75 | bgfx::setUniform(baseColorFactorUniform, glm::value_ptr(material.baseColorFactor)); 76 | bgfx::setUniform(metallicRoughnessNormalOcclusionFactorUniform, factorValues); 77 | glm::vec4 emissiveFactor = glm::vec4(material.emissiveFactor, 0.0f); 78 | bgfx::setUniform(emissiveFactorUniform, glm::value_ptr(emissiveFactor)); 79 | 80 | float hasTexturesValues[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; 81 | 82 | auto setTextureOrDefault = [&](uint8_t stage, bgfx::UniformHandle uniform, bgfx::TextureHandle texture) -> bool { 83 | bool valid = bgfx::isValid(texture); 84 | if(!valid) 85 | texture = defaultTexture; 86 | bgfx::setTexture(stage, uniform, texture); 87 | return valid; 88 | }; 89 | 90 | const uint32_t hasTexturesMask = 0 91 | | ((setTextureOrDefault(Samplers::PBR_BASECOLOR, baseColorSampler, material.baseColorTexture) ? 1 : 0) << 0) 92 | | ((setTextureOrDefault(Samplers::PBR_METALROUGHNESS, metallicRoughnessSampler, material.metallicRoughnessTexture) ? 1 : 0) << 1) 93 | | ((setTextureOrDefault(Samplers::PBR_NORMAL, normalSampler, material.normalTexture) ? 1 : 0) << 2) 94 | | ((setTextureOrDefault(Samplers::PBR_OCCLUSION, occlusionSampler, material.occlusionTexture) ? 1 : 0) << 3) 95 | | ((setTextureOrDefault(Samplers::PBR_EMISSIVE, emissiveSampler, material.emissiveTexture) ? 1 : 0) << 4); 96 | hasTexturesValues[0] = static_cast(hasTexturesMask); 97 | 98 | bgfx::setUniform(hasTexturesUniform, hasTexturesValues); 99 | 100 | float multipleScatteringValues[4] = { 101 | multipleScatteringEnabled ? 1.0f : 0.0f, whiteFurnaceEnabled ? WHITE_FURNACE_RADIANCE : 0.0f, 0.0f, 0.0f 102 | }; 103 | bgfx::setUniform(multipleScatteringUniform, multipleScatteringValues); 104 | 105 | uint64_t state = 0; 106 | if(material.blend) 107 | state |= BGFX_STATE_BLEND_ALPHA; 108 | if(!material.doubleSided) 109 | state |= BGFX_STATE_CULL_CW; 110 | return state; 111 | } 112 | 113 | void PBRShader::bindAlbedoLUT(bool compute) 114 | { 115 | if(compute) 116 | bgfx::setImage(Samplers::PBR_ALBEDO_LUT, albedoLUTTexture, 0, bgfx::Access::Write); 117 | else 118 | bgfx::setTexture(Samplers::PBR_ALBEDO_LUT, albedoLUTSampler, albedoLUTTexture); 119 | } 120 | -------------------------------------------------------------------------------- /src/Renderer/PBRShader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct Material; 6 | 7 | class PBRShader 8 | { 9 | public: 10 | void initialize(); 11 | void shutdown(); 12 | 13 | void generateAlbedoLUT(); 14 | 15 | uint64_t bindMaterial(const Material& material); 16 | void bindAlbedoLUT(bool compute = false); 17 | 18 | static constexpr float WHITE_FURNACE_RADIANCE = 1.0f; 19 | 20 | bool multipleScatteringEnabled = true; 21 | bool whiteFurnaceEnabled = false; 22 | 23 | private: 24 | static constexpr uint16_t ALBEDO_LUT_SIZE = 32; 25 | static constexpr uint16_t ALBEDO_LUT_THREADS = 32; 26 | 27 | bgfx::UniformHandle baseColorFactorUniform = BGFX_INVALID_HANDLE; 28 | bgfx::UniformHandle metallicRoughnessNormalOcclusionFactorUniform = BGFX_INVALID_HANDLE; 29 | bgfx::UniformHandle emissiveFactorUniform = BGFX_INVALID_HANDLE; 30 | bgfx::UniformHandle hasTexturesUniform = BGFX_INVALID_HANDLE; 31 | bgfx::UniformHandle multipleScatteringUniform = BGFX_INVALID_HANDLE; 32 | bgfx::UniformHandle albedoLUTSampler = BGFX_INVALID_HANDLE; 33 | bgfx::UniformHandle baseColorSampler = BGFX_INVALID_HANDLE; 34 | bgfx::UniformHandle metallicRoughnessSampler = BGFX_INVALID_HANDLE; 35 | bgfx::UniformHandle normalSampler = BGFX_INVALID_HANDLE; 36 | bgfx::UniformHandle occlusionSampler = BGFX_INVALID_HANDLE; 37 | bgfx::UniformHandle emissiveSampler = BGFX_INVALID_HANDLE; 38 | 39 | bgfx::TextureHandle albedoLUTTexture = BGFX_INVALID_HANDLE; 40 | bgfx::TextureHandle defaultTexture = BGFX_INVALID_HANDLE; 41 | 42 | bgfx::ProgramHandle albedoLUTProgram = BGFX_INVALID_HANDLE; 43 | }; 44 | -------------------------------------------------------------------------------- /src/Renderer/Renderer.cpp: -------------------------------------------------------------------------------- 1 | #include "Renderer.h" 2 | 3 | #include "Scene/Scene.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | bgfx::VertexLayout Renderer::PosVertex::layout; 16 | 17 | Renderer::Renderer(const Scene* scene) : scene(scene) { } 18 | 19 | void Renderer::initialize() 20 | { 21 | PosVertex::init(); 22 | 23 | blitSampler = bgfx::createUniform("s_texColor", bgfx::UniformType::Sampler); 24 | camPosUniform = bgfx::createUniform("u_camPos", bgfx::UniformType::Vec4); 25 | normalMatrixUniform = bgfx::createUniform("u_normalMatrix", bgfx::UniformType::Mat3); 26 | exposureVecUniform = bgfx::createUniform("u_exposureVec", bgfx::UniformType::Vec4); 27 | tonemappingModeVecUniform = bgfx::createUniform("u_tonemappingModeVec", bgfx::UniformType::Vec4); 28 | 29 | // triangle used for blitting 30 | constexpr float BOTTOM = -1.0f, TOP = 3.0f, LEFT = -1.0f, RIGHT = 3.0f; 31 | const PosVertex vertices[3] = { { LEFT, BOTTOM, 0.0f }, { RIGHT, BOTTOM, 0.0f }, { LEFT, TOP, 0.0f } }; 32 | blitTriangleBuffer = bgfx::createVertexBuffer(bgfx::copy(&vertices, sizeof(vertices)), PosVertex::layout); 33 | 34 | char vsName[128], fsName[128]; 35 | bx::snprintf(vsName, BX_COUNTOF(vsName), "%s%s", shaderDir(), "vs_tonemap.bin"); 36 | bx::snprintf(fsName, BX_COUNTOF(fsName), "%s%s", shaderDir(), "fs_tonemap.bin"); 37 | blitProgram = bigg::loadProgram(vsName, fsName); 38 | 39 | pbr.initialize(); 40 | pbr.generateAlbedoLUT(); 41 | lights.initialize(); 42 | 43 | onInitialize(); 44 | 45 | // finish any queued precomputations before rendering the scene 46 | bgfx::frame(); 47 | } 48 | 49 | void Renderer::reset(uint16_t width, uint16_t height) 50 | { 51 | if(!bgfx::isValid(frameBuffer)) 52 | { 53 | frameBuffer = createFrameBuffer(true, true); 54 | bgfx::setName(frameBuffer, "Render framebuffer (pre-postprocessing)"); 55 | } 56 | this->width = width; 57 | this->height = height; 58 | 59 | onReset(); 60 | } 61 | 62 | void Renderer::render(float dt) 63 | { 64 | time += dt; 65 | 66 | if(scene->loaded) 67 | { 68 | glm::vec4 camPos = glm::vec4(scene->camera.position(), 1.0f); 69 | bgfx::setUniform(camPosUniform, glm::value_ptr(camPos)); 70 | 71 | glm::vec3 linear = pbr.whiteFurnaceEnabled 72 | ? glm::vec3(PBRShader::WHITE_FURNACE_RADIANCE) 73 | : glm::convertSRGBToLinear(scene->skyColor); // tonemapping expects linear colors 74 | glm::u8vec3 result = glm::u8vec3(glm::round(glm::clamp(linear, 0.0f, 1.0f) * 255.0f)); 75 | clearColor = (result[0] << 24) | (result[1] << 16) | (result[2] << 8) | 255; 76 | } 77 | else 78 | clearColor = 0x303030FF; // gray 79 | 80 | onRender(dt); 81 | blitToScreen(MAX_VIEW); 82 | 83 | // bigg doesn't do this 84 | bgfx::setViewName(MAX_VIEW + 1, "imgui"); 85 | } 86 | 87 | void Renderer::shutdown() 88 | { 89 | onShutdown(); 90 | 91 | pbr.shutdown(); 92 | lights.shutdown(); 93 | 94 | bgfx::destroy(blitProgram); 95 | bgfx::destroy(blitSampler); 96 | bgfx::destroy(camPosUniform); 97 | bgfx::destroy(normalMatrixUniform); 98 | bgfx::destroy(exposureVecUniform); 99 | bgfx::destroy(tonemappingModeVecUniform); 100 | bgfx::destroy(blitTriangleBuffer); 101 | if(bgfx::isValid(frameBuffer)) 102 | bgfx::destroy(frameBuffer); 103 | 104 | blitProgram = BGFX_INVALID_HANDLE; 105 | blitSampler = camPosUniform = normalMatrixUniform = exposureVecUniform = tonemappingModeVecUniform = 106 | BGFX_INVALID_HANDLE; 107 | blitTriangleBuffer = BGFX_INVALID_HANDLE; 108 | frameBuffer = BGFX_INVALID_HANDLE; 109 | 110 | for(bgfx::ViewId i = 0; i < MAX_VIEW; i++) 111 | { 112 | bgfx::resetView(i); 113 | } 114 | 115 | bgfx::discard(); 116 | } 117 | 118 | void Renderer::setVariable(const std::string& name, const std::string& val) 119 | { 120 | variables[name] = val; 121 | } 122 | 123 | void Renderer::setTonemappingMode(TonemappingMode mode) 124 | { 125 | tonemappingMode = mode; 126 | } 127 | 128 | void Renderer::setMultipleScattering(bool enabled) 129 | { 130 | pbr.multipleScatteringEnabled = enabled; 131 | } 132 | 133 | void Renderer::setWhiteFurnace(bool enabled) 134 | { 135 | pbr.whiteFurnaceEnabled = enabled; 136 | } 137 | 138 | bool Renderer::supported() 139 | { 140 | const bgfx::Caps* caps = bgfx::getCaps(); 141 | return 142 | // SDR color attachment 143 | (caps->formats[bgfx::TextureFormat::BGRA8] & BGFX_CAPS_FORMAT_TEXTURE_FRAMEBUFFER) != 0 && 144 | // HDR color attachment 145 | (caps->formats[bgfx::TextureFormat::RGBA16F] & BGFX_CAPS_FORMAT_TEXTURE_FRAMEBUFFER) != 0; 146 | } 147 | 148 | void Renderer::setViewProjection(bgfx::ViewId view) 149 | { 150 | // view matrix 151 | viewMat = scene->camera.matrix(); 152 | // projection matrix 153 | bx::mtxProj(glm::value_ptr(projMat), 154 | scene->camera.fov, 155 | float(width) / height, 156 | scene->camera.zNear, 157 | scene->camera.zFar, 158 | bgfx::getCaps()->homogeneousDepth, 159 | bx::Handness::Left); 160 | bgfx::setViewTransform(view, glm::value_ptr(viewMat), glm::value_ptr(projMat)); 161 | } 162 | 163 | void Renderer::setNormalMatrix(const glm::mat4& modelMat) 164 | { 165 | // usually the normal matrix is based on the model view matrix 166 | // but shading is done in world space (not eye space) so it's just the model matrix 167 | //glm::mat4 modelViewMat = viewMat * modelMat; 168 | 169 | // if we don't do non-uniform scaling, the normal matrix is the same as the model-view matrix 170 | // (only the magnitude of the normal is changed, but we normalize either way) 171 | //glm::mat3 normalMat = glm::mat3(modelMat); 172 | 173 | // use adjugate instead of inverse 174 | // see https://github.com/graphitemaster/normals_revisited#the-details-of-transforming-normals 175 | // cofactor is the transpose of the adjugate 176 | glm::mat3 normalMat = glm::transpose(glm::adjugate(glm::mat3(modelMat))); 177 | bgfx::setUniform(normalMatrixUniform, glm::value_ptr(normalMat)); 178 | } 179 | 180 | void Renderer::blitToScreen(bgfx::ViewId view) 181 | { 182 | bgfx::setViewName(view, "Tonemapping"); 183 | bgfx::setViewClear(view, BGFX_CLEAR_NONE); 184 | bgfx::setViewRect(view, 0, 0, width, height); 185 | bgfx::setViewFrameBuffer(view, BGFX_INVALID_HANDLE); 186 | bgfx::setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_CULL_CW); 187 | bgfx::TextureHandle frameBufferTexture = bgfx::getTexture(frameBuffer, 0); 188 | bgfx::setTexture(0, blitSampler, frameBufferTexture); 189 | float exposureVec[4] = { scene->loaded ? scene->camera.exposure : 1.0f }; 190 | bgfx::setUniform(exposureVecUniform, exposureVec); 191 | float tonemappingModeVec[4] = { (float)tonemappingMode }; 192 | bgfx::setUniform(tonemappingModeVecUniform, tonemappingModeVec); 193 | bgfx::setVertexBuffer(0, blitTriangleBuffer); 194 | bgfx::submit(view, blitProgram); 195 | } 196 | 197 | bgfx::TextureFormat::Enum Renderer::findDepthFormat(uint64_t textureFlags, bool stencil) 198 | { 199 | const bgfx::TextureFormat::Enum depthFormats[] = { bgfx::TextureFormat::D16, bgfx::TextureFormat::D32 }; 200 | 201 | const bgfx::TextureFormat::Enum depthStencilFormats[] = { bgfx::TextureFormat::D24S8 }; 202 | 203 | const bgfx::TextureFormat::Enum* formats = stencil ? depthStencilFormats : depthFormats; 204 | size_t count = stencil ? BX_COUNTOF(depthStencilFormats) : BX_COUNTOF(depthFormats); 205 | 206 | bgfx::TextureFormat::Enum depthFormat = bgfx::TextureFormat::Count; 207 | for(size_t i = 0; i < count; i++) 208 | { 209 | if(bgfx::isTextureValid(0, false, 1, formats[i], textureFlags)) 210 | { 211 | depthFormat = formats[i]; 212 | break; 213 | } 214 | } 215 | 216 | assert(depthFormat != bgfx::TextureFormat::Enum::Count); 217 | 218 | return depthFormat; 219 | } 220 | 221 | bgfx::FrameBufferHandle Renderer::createFrameBuffer(bool hdr, bool depth) 222 | { 223 | bgfx::TextureHandle textures[2]; 224 | uint8_t attachments = 0; 225 | 226 | const uint64_t samplerFlags = BGFX_SAMPLER_MIN_POINT | BGFX_SAMPLER_MAG_POINT | BGFX_SAMPLER_MIP_POINT | 227 | BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP; 228 | 229 | bgfx::TextureFormat::Enum format = 230 | hdr ? bgfx::TextureFormat::RGBA16F : bgfx::TextureFormat::BGRA8; // BGRA is often faster (internal GPU format) 231 | assert(bgfx::isTextureValid(0, false, 1, format, BGFX_TEXTURE_RT | samplerFlags)); 232 | textures[attachments++] = 233 | bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, format, BGFX_TEXTURE_RT | samplerFlags); 234 | 235 | if(depth) 236 | { 237 | bgfx::TextureFormat::Enum depthFormat = findDepthFormat(BGFX_TEXTURE_RT_WRITE_ONLY | samplerFlags); 238 | assert(depthFormat != bgfx::TextureFormat::Enum::Count); 239 | textures[attachments++] = bgfx::createTexture2D( 240 | bgfx::BackbufferRatio::Equal, false, 1, depthFormat, BGFX_TEXTURE_RT_WRITE_ONLY | samplerFlags); 241 | } 242 | 243 | bgfx::FrameBufferHandle fb = bgfx::createFrameBuffer(attachments, textures, true); 244 | 245 | if(!bgfx::isValid(fb)) 246 | Log->warn("Failed to create framebuffer"); 247 | 248 | return fb; 249 | } 250 | 251 | const char* Renderer::shaderDir() 252 | { 253 | const char* path = "???"; 254 | 255 | switch(bgfx::getRendererType()) 256 | { 257 | case bgfx::RendererType::Noop: 258 | case bgfx::RendererType::Direct3D9: 259 | path = "shaders/dx9/"; 260 | break; 261 | case bgfx::RendererType::Direct3D11: 262 | case bgfx::RendererType::Direct3D12: 263 | path = "shaders/dx11/"; 264 | break; 265 | case bgfx::RendererType::Gnm: 266 | break; 267 | case bgfx::RendererType::Metal: 268 | path = "shaders/metal/"; 269 | break; 270 | case bgfx::RendererType::OpenGL: 271 | path = "shaders/glsl/"; 272 | break; 273 | case bgfx::RendererType::OpenGLES: 274 | path = "shaders/essl/"; 275 | break; 276 | case bgfx::RendererType::Vulkan: 277 | path = "shaders/spirv/"; 278 | break; 279 | default: 280 | assert(false); 281 | break; 282 | } 283 | 284 | return path; 285 | } 286 | -------------------------------------------------------------------------------- /src/Renderer/Renderer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "Renderer/PBRShader.h" 5 | #include "Renderer/LightShader.h" 6 | #include 7 | #include 8 | #include 9 | 10 | class Scene; 11 | 12 | class Renderer 13 | { 14 | public: 15 | Renderer(const Scene* scene); 16 | virtual ~Renderer() { } 17 | 18 | void initialize(); 19 | void reset(uint16_t width, uint16_t height); 20 | void render(float dt); 21 | void shutdown(); 22 | 23 | void setVariable(const std::string& name, const std::string& val); 24 | 25 | enum class TonemappingMode : int 26 | { 27 | NONE = 0, 28 | EXPONENTIAL, 29 | REINHARD, 30 | REINHARD_LUM, 31 | HABLE, 32 | DUIKER, 33 | ACES, 34 | ACES_LUM 35 | }; 36 | 37 | void setTonemappingMode(TonemappingMode mode); 38 | void setMultipleScattering(bool enabled); 39 | void setWhiteFurnace(bool enabled); 40 | 41 | static bool supported(); 42 | static const char* shaderDir(); 43 | 44 | // subclasses should override these 45 | 46 | // the first reset happens before initialize 47 | virtual void onInitialize() { } 48 | // window resize/flags changed (MSAA, V-Sync, ...) 49 | virtual void onReset() { } 50 | virtual void onRender(float dt) = 0; 51 | virtual void onShutdown() { } 52 | 53 | // buffers for debug output (display in the UI) 54 | 55 | struct TextureBuffer 56 | { 57 | bgfx::TextureHandle handle; 58 | const char* name; 59 | }; 60 | 61 | TextureBuffer* buffers = nullptr; 62 | 63 | // final output 64 | // used for tonemapping 65 | bgfx::FrameBufferHandle frameBuffer = BGFX_INVALID_HANDLE; 66 | 67 | protected: 68 | struct PosVertex 69 | { 70 | float x; 71 | float y; 72 | float z; 73 | 74 | static void init() 75 | { 76 | layout.begin().add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float).end(); 77 | } 78 | 79 | static bgfx::VertexLayout layout; 80 | }; 81 | 82 | static constexpr bgfx::ViewId MAX_VIEW = 199; // imgui in bigg uses view 200 83 | 84 | void setViewProjection(bgfx::ViewId view); 85 | void setNormalMatrix(const glm::mat4& modelMat); 86 | 87 | void blitToScreen(bgfx::ViewId view = MAX_VIEW); 88 | 89 | static bgfx::TextureFormat::Enum findDepthFormat(uint64_t textureFlags, bool stencil = false); 90 | static bgfx::FrameBufferHandle createFrameBuffer(bool hdr = true, bool depth = true); 91 | 92 | std::unordered_map variables; 93 | 94 | TonemappingMode tonemappingMode = TonemappingMode::NONE; 95 | 96 | const Scene* scene = nullptr; 97 | 98 | uint16_t width = 0; 99 | uint16_t height = 0; 100 | 101 | PBRShader pbr; 102 | LightShader lights; 103 | 104 | uint32_t clearColor = 0; 105 | float time = 0.0f; 106 | 107 | // set by setViewProjection() 108 | glm::mat4 viewMat = glm::mat4(1.0); 109 | glm::mat4 projMat = glm::mat4(1.0); 110 | 111 | bgfx::VertexBufferHandle blitTriangleBuffer = BGFX_INVALID_HANDLE; 112 | 113 | private: 114 | bgfx::ProgramHandle blitProgram = BGFX_INVALID_HANDLE; 115 | bgfx::UniformHandle blitSampler = BGFX_INVALID_HANDLE; 116 | bgfx::UniformHandle camPosUniform = BGFX_INVALID_HANDLE; 117 | bgfx::UniformHandle normalMatrixUniform = BGFX_INVALID_HANDLE; 118 | bgfx::UniformHandle exposureVecUniform = BGFX_INVALID_HANDLE; 119 | bgfx::UniformHandle tonemappingModeVecUniform = BGFX_INVALID_HANDLE; 120 | }; 121 | -------------------------------------------------------------------------------- /src/Renderer/Samplers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class Samplers 6 | { 7 | // these should be the same as in samplers.sh for shaders 8 | 9 | public: 10 | static const uint8_t PBR_ALBEDO_LUT = 0; 11 | 12 | static const uint8_t PBR_BASECOLOR = 1; 13 | static const uint8_t PBR_METALROUGHNESS = 2; 14 | static const uint8_t PBR_NORMAL = 3; 15 | static const uint8_t PBR_OCCLUSION = 4; 16 | static const uint8_t PBR_EMISSIVE = 5; 17 | 18 | static const uint8_t LIGHTS_POINTLIGHTS = 6; 19 | 20 | static const uint8_t CLUSTERS_CLUSTERS = 7; 21 | static const uint8_t CLUSTERS_LIGHTINDICES = 8; 22 | static const uint8_t CLUSTERS_LIGHTGRID = 9; 23 | static const uint8_t CLUSTERS_ATOMICINDEX = 10; 24 | 25 | static const uint8_t DEFERRED_DIFFUSE_A = 7; 26 | static const uint8_t DEFERRED_NORMAL = 8; 27 | static const uint8_t DEFERRED_F0_METALLIC = 9; 28 | static const uint8_t DEFERRED_EMISSIVE_OCCLUSION = 10; 29 | static const uint8_t DEFERRED_DEPTH = 11; 30 | }; 31 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/clusters.sh: -------------------------------------------------------------------------------- 1 | #ifndef CLUSTERS_SH_HEADER_GUARD 2 | #define CLUSTERS_SH_HEADER_GUARD 3 | 4 | #include 5 | #include "samplers.sh" 6 | #include "util.sh" 7 | 8 | // taken from Doom 9 | // http://advances.realtimerendering.com/s2016/Siggraph2016_idTech6.pdf 10 | 11 | #define CLUSTERS_X 16 12 | #define CLUSTERS_Y 8 13 | #define CLUSTERS_Z 24 14 | 15 | // workgroup size of the culling compute shader 16 | // D3D compute shaders only allow up to 1024 threads per workgroup 17 | // GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS also only guarantees 1024 18 | #define CLUSTERS_X_THREADS 16 19 | #define CLUSTERS_Y_THREADS 8 20 | #define CLUSTERS_Z_THREADS 4 21 | 22 | #define MAX_LIGHTS_PER_CLUSTER 100 23 | 24 | uniform vec4 u_clusterSizesVec; // cluster size in screen coordinates (pixels) 25 | uniform vec4 u_zNearFarVec; 26 | 27 | #define u_clusterSizes u_clusterSizesVec.xy 28 | #define u_zNear u_zNearFarVec.x 29 | #define u_zFar u_zNearFarVec.y 30 | 31 | #ifdef WRITE_CLUSTERS 32 | #define CLUSTER_BUFFER BUFFER_RW 33 | #else 34 | #define CLUSTER_BUFFER BUFFER_RO 35 | #endif 36 | 37 | // light indices belonging to clusters 38 | CLUSTER_BUFFER(b_clusterLightIndices, uint, SAMPLER_CLUSTERS_LIGHTINDICES); 39 | // for each cluster: (start index in b_clusterLightIndices, number of point lights, empty, empty) 40 | CLUSTER_BUFFER(b_clusterLightGrid, uvec4, SAMPLER_CLUSTERS_LIGHTGRID); 41 | 42 | // these are only needed for building clusters and light culling, not in the fragment shader 43 | #ifdef WRITE_CLUSTERS 44 | // list of clusters (2 vec4's each, min + max pos for AABB) 45 | CLUSTER_BUFFER(b_clusters, vec4, SAMPLER_CLUSTERS_CLUSTERS); 46 | // atomic counter for building the light grid 47 | // must be reset to 0 every frame 48 | CLUSTER_BUFFER(b_globalIndex, uint, SAMPLER_CLUSTERS_ATOMICINDEX); 49 | #endif 50 | 51 | struct Cluster 52 | { 53 | vec3 minBounds; 54 | vec3 maxBounds; 55 | }; 56 | 57 | struct LightGrid 58 | { 59 | uint offset; 60 | uint pointLights; 61 | }; 62 | 63 | #ifdef WRITE_CLUSTERS 64 | Cluster getCluster(uint index) 65 | { 66 | Cluster cluster; 67 | cluster.minBounds = b_clusters[2 * index + 0].xyz; 68 | cluster.maxBounds = b_clusters[2 * index + 1].xyz; 69 | return cluster; 70 | } 71 | #endif 72 | 73 | LightGrid getLightGrid(uint cluster) 74 | { 75 | uvec4 gridvec = b_clusterLightGrid[cluster]; 76 | LightGrid grid; 77 | grid.offset = gridvec.x; 78 | grid.pointLights = gridvec.y; 79 | return grid; 80 | } 81 | 82 | uint getGridLightIndex(uint start, uint offset) 83 | { 84 | return b_clusterLightIndices[start + offset]; 85 | } 86 | 87 | // cluster depth index from depth in screen coordinates (gl_FragCoord.z) 88 | uint getClusterZIndex(float screenDepth) 89 | { 90 | // this can be calculated on the CPU and passed as a uniform 91 | // only leaving it here to keep most of the relevant code in the shaders for learning purposes 92 | float scale = float(CLUSTERS_Z) / log(u_zFar / u_zNear); 93 | float bias = -(float(CLUSTERS_Z) * log(u_zNear) / log(u_zFar / u_zNear)); 94 | 95 | float eyeDepth = screen2EyeDepth(screenDepth, u_zNear, u_zFar); 96 | uint zIndex = uint(max(log(eyeDepth) * scale + bias, 0.0)); 97 | return zIndex; 98 | } 99 | 100 | // cluster index from fragment position in window coordinates (gl_FragCoord) 101 | uint getClusterIndex(vec4 fragCoord) 102 | { 103 | uint zIndex = getClusterZIndex(fragCoord.z); 104 | uvec3 indices = uvec3(uvec2(fragCoord.xy / u_clusterSizes.xy), zIndex); 105 | uint cluster = (CLUSTERS_X * CLUSTERS_Y) * indices.z + 106 | CLUSTERS_X * indices.y + 107 | indices.x; 108 | return cluster; 109 | } 110 | 111 | #endif // CLUSTERS_SH_HEADER_GUARD 112 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/colormap.sh: -------------------------------------------------------------------------------- 1 | #ifndef COLORMAP_SH_HEADER_GUARD 2 | #define COLORMAP_SH_HEADER_GUARD 3 | 4 | // Copyright 2019 Google LLC. 5 | // SPDX-License-Identifier: Apache-2.0 6 | 7 | // Polynomial approximation in GLSL for the Turbo colormap 8 | // Original LUT: https://gist.github.com/mikhailov-work/ee72ba4191942acecc03fe6da94fc73f 9 | 10 | // Authors: 11 | // Colormap Design: Anton Mikhailov (mikhailov@google.com) 12 | // GLSL Approximation: Ruofei Du (ruofei@google.com) 13 | 14 | // See also: https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html 15 | 16 | vec3 turboColormap(float x) 17 | { 18 | // show clipping 19 | if(x < 0.0) 20 | return vec3_splat(0.0); 21 | else if(x > 1.0) 22 | return vec3_splat(1.0); 23 | 24 | const vec4 kRedVec4 = vec4(0.13572138, 4.61539260, -42.66032258, 132.13108234); 25 | const vec4 kGreenVec4 = vec4(0.09140261, 2.19418839, 4.84296658, -14.18503333); 26 | const vec4 kBlueVec4 = vec4(0.10667330, 12.64194608, -60.58204836, 110.36276771); 27 | const vec2 kRedVec2 = vec2(-152.94239396, 59.28637943); 28 | const vec2 kGreenVec2 = vec2(4.27729857, 2.82956604); 29 | const vec2 kBlueVec2 = vec2(-89.90310912, 27.34824973); 30 | 31 | x = saturate(x); 32 | vec4 v4 = vec4(1.0, x, x * x, x * x * x); 33 | vec2 v2 = v4.zw * v4.z; 34 | return vec3( 35 | dot(v4, kRedVec4) + dot(v2, kRedVec2), 36 | dot(v4, kGreenVec4) + dot(v2, kGreenVec2), 37 | dot(v4, kBlueVec4) + dot(v2, kBlueVec2) 38 | ); 39 | } 40 | 41 | #endif // COLORMAP_SH_HEADER_GUARD 42 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/cs_clustered_clusterbuilding.sc: -------------------------------------------------------------------------------- 1 | #define WRITE_CLUSTERS 2 | 3 | #include 4 | #include "clusters.sh" 5 | #include "util.sh" 6 | 7 | // compute shader to calculate light cluster min/max AABB in eye space 8 | // largely inspired by http://www.aortiz.me/2018/12/21/CG.html 9 | // z-subdivision concept from http://advances.realtimerendering.com/s2016/Siggraph2016_idTech6.pdf 10 | 11 | // bgfx doesn't define this in shaders 12 | #define gl_WorkGroupSize uvec3(CLUSTERS_X_THREADS, CLUSTERS_Y_THREADS, CLUSTERS_Z_THREADS) 13 | 14 | // each thread handles one cluster 15 | NUM_THREADS(CLUSTERS_X_THREADS, CLUSTERS_Y_THREADS, CLUSTERS_Z_THREADS) 16 | void main() 17 | { 18 | // index calculation must match the inverse operation in the fragment shader (see getClusterIndex) 19 | uint clusterIndex = gl_GlobalInvocationID.z * gl_WorkGroupSize.x * gl_WorkGroupSize.y + 20 | gl_GlobalInvocationID.y * gl_WorkGroupSize.x + 21 | gl_GlobalInvocationID.x; 22 | 23 | // calculate min (bottom left) and max (top right) xy in screen coordinates 24 | vec4 minScreen = vec4( gl_GlobalInvocationID.xy * u_clusterSizes.xy, 1.0, 1.0); 25 | vec4 maxScreen = vec4((gl_GlobalInvocationID.xy + vec2(1, 1)) * u_clusterSizes.xy, 1.0, 1.0); 26 | 27 | // -> eye coordinates 28 | // z is the camera far plane (1 in screen coordinates) 29 | vec3 minEye = screen2Eye(minScreen).xyz; 30 | vec3 maxEye = screen2Eye(maxScreen).xyz; 31 | 32 | // calculate near and far depth edges of the cluster 33 | float clusterNear = u_zNear * pow(u_zFar / u_zNear, gl_GlobalInvocationID.z / float(CLUSTERS_Z)); 34 | float clusterFar = u_zNear * pow(u_zFar / u_zNear, (gl_GlobalInvocationID.z + 1) / float(CLUSTERS_Z)); 35 | 36 | // this calculates the intersection between: 37 | // - a line from the camera (origin) to the eye point (at the camera's far plane) 38 | // - the cluster's z-planes (near + far) 39 | // we could divide by u_zFar as well 40 | vec3 minNear = minEye * clusterNear / minEye.z; 41 | vec3 minFar = minEye * clusterFar / minEye.z; 42 | vec3 maxNear = maxEye * clusterNear / maxEye.z; 43 | vec3 maxFar = maxEye * clusterFar / maxEye.z; 44 | 45 | // get extent of the cluster in all dimensions (axis-aligned bounding box) 46 | // there is some overlap here but it's easier to calculate intersections with AABB 47 | vec3 minBounds = min(min(minNear, minFar), min(maxNear, maxFar)); 48 | vec3 maxBounds = max(max(minNear, minFar), max(maxNear, maxFar)); 49 | 50 | b_clusters[2 * clusterIndex + 0] = vec4(minBounds, 1.0); 51 | b_clusters[2 * clusterIndex + 1] = vec4(maxBounds, 1.0); 52 | } 53 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/cs_clustered_lightculling.sc: -------------------------------------------------------------------------------- 1 | #define WRITE_CLUSTERS 2 | 3 | #include 4 | #include "lights.sh" 5 | #include "clusters.sh" 6 | 7 | // compute shader to cull lights against cluster bounds 8 | // builds a light grid that holds indices of lights for each cluster 9 | // largely inspired by http://www.aortiz.me/2018/12/21/CG.html 10 | 11 | // point lights only for now 12 | bool pointLightIntersectsCluster(PointLight light, Cluster cluster); 13 | 14 | #define gl_WorkGroupSize uvec3(CLUSTERS_X_THREADS, CLUSTERS_Y_THREADS, CLUSTERS_Z_THREADS) 15 | #define GROUP_SIZE (CLUSTERS_X_THREADS * CLUSTERS_Y_THREADS * CLUSTERS_Z_THREADS) 16 | 17 | // light cache for the current workgroup 18 | // group shared memory has lower latency than global memory 19 | 20 | // there's no guarantee on the available shared memory 21 | // as a guideline the minimum value of GL_MAX_COMPUTE_SHARED_MEMORY_SIZE is 32KB 22 | // with a workgroup size of 16*8*4 this is 64 bytes per light 23 | // however, using all available memory would limit the compute shader invocation to only 1 workgroup 24 | SHARED PointLight lights[GROUP_SIZE]; 25 | 26 | // each thread handles one cluster 27 | NUM_THREADS(CLUSTERS_X_THREADS, CLUSTERS_Y_THREADS, CLUSTERS_Z_THREADS) 28 | void main() 29 | { 30 | // local thread variables 31 | // hold the result of light culling for this cluster 32 | uint visibleLights[MAX_LIGHTS_PER_CLUSTER]; 33 | uint visibleCount = 0; 34 | 35 | // the way we calculate the index doesn't really matter here since we write to the same index in the light grid as we read from the cluster buffer 36 | uint clusterIndex = gl_GlobalInvocationID.z * gl_WorkGroupSize.x * gl_WorkGroupSize.y + 37 | gl_GlobalInvocationID.y * gl_WorkGroupSize.x + 38 | gl_GlobalInvocationID.x; 39 | 40 | Cluster cluster = getCluster(clusterIndex); 41 | 42 | // we have a cache of GROUP_SIZE lights 43 | // have to run this loop several times if we have more than GROUP_SIZE lights 44 | uint lightCount = pointLightCount(); 45 | uint lightOffset = 0; 46 | while(lightOffset < lightCount) 47 | { 48 | // read GROUP_SIZE lights into shared memory 49 | // each thread copies one light 50 | uint batchSize = min(GROUP_SIZE, lightCount - lightOffset); 51 | 52 | if(uint(gl_LocalInvocationIndex) < batchSize) 53 | { 54 | uint lightIndex = lightOffset + gl_LocalInvocationIndex; 55 | PointLight light = getPointLight(lightIndex); 56 | // transform to view space (expected by pointLightAffectsCluster) 57 | // do it here once rather than for each cluster later 58 | light.position = mul(u_view, vec4(light.position, 1.0)).xyz; 59 | lights[gl_LocalInvocationIndex] = light; 60 | } 61 | 62 | // wait for all threads to finish copying 63 | barrier(); 64 | 65 | // each thread is one cluster and checks against all lights in the cache 66 | for(uint i = 0; i < batchSize; i++) 67 | { 68 | Cluster cluster = getCluster(clusterIndex); 69 | if(visibleCount < MAX_LIGHTS_PER_CLUSTER && pointLightIntersectsCluster(lights[i], cluster)) 70 | { 71 | visibleLights[visibleCount] = lightOffset + i; 72 | visibleCount++; 73 | } 74 | } 75 | 76 | lightOffset += batchSize; 77 | } 78 | 79 | // wait for all threads to finish checking lights 80 | barrier(); 81 | 82 | // get a unique index into the light index list where we can write this cluster's lights 83 | uint offset = 0; 84 | atomicFetchAndAdd(b_globalIndex[0], visibleCount, offset); 85 | // copy indices of lights 86 | for(uint i = 0; i < visibleCount; i++) 87 | { 88 | b_clusterLightIndices[offset + i] = visibleLights[i]; 89 | } 90 | 91 | // write light grid for this cluster 92 | b_clusterLightGrid[clusterIndex] = uvec4(offset, visibleCount, 0, 0); 93 | } 94 | 95 | // check if light radius extends into the cluster 96 | bool pointLightIntersectsCluster(PointLight light, Cluster cluster) 97 | { 98 | // NOTE: expects light.position to be in view space like the cluster bounds 99 | // global light list has world space coordinates, but we transform the 100 | // coordinates in the shared array of lights after copying 101 | 102 | // get closest point to sphere center 103 | vec3 closest = max(cluster.minBounds, min(light.position, cluster.maxBounds)); 104 | // check if point is inside the sphere 105 | vec3 dist = closest - light.position; 106 | return dot(dist, dist) <= (light.radius * light.radius); 107 | } 108 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/cs_clustered_reset_counter.sc: -------------------------------------------------------------------------------- 1 | #define WRITE_CLUSTERS 2 | 3 | #include 4 | #include "clusters.sh" 5 | 6 | NUM_THREADS(1, 1, 1) 7 | void main() 8 | { 9 | if(gl_GlobalInvocationID.x == 0) 10 | { 11 | // reset the atomic counter for the light grid generation 12 | // writable compute buffers can't be updated by CPU so do it here 13 | b_globalIndex[0] = 0; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/cs_multiple_scattering_lut.sc: -------------------------------------------------------------------------------- 1 | #define WRITE_LUT 2 | 3 | #include 4 | #include "pbr.sh" 5 | 6 | // compute shader to calculate a lookup table for multiple scattering correction 7 | 8 | // TODO fix black pixels at grazing angles (NaN?) 9 | 10 | // Turquin. 2018. Practical multiple scattering compensation for microfacet models. 11 | // https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf 12 | 13 | // some importance-sampling code taken from https://bruop.github.io/ibl/ 14 | 15 | // Turquin recommends 32x32 16 | // the functions are fairly smooth so this is enough 17 | #define LUT_SIZE 32 18 | #define THREADS LUT_SIZE 19 | 20 | // number of samples for approximating the integral 21 | #define NUM_SAMPLES 1024 22 | 23 | // http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html 24 | vec2 hammersley_2d(uint i, uint N) 25 | { 26 | uint bits = (i << 16u) | (i >> 16u); 27 | bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); 28 | bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); 29 | bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); 30 | bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); 31 | float ri = float(bits) * 2.3283064365386963e-10; // / (1.0 << 32) 32 | return vec2(float(i) / float(N), ri); 33 | } 34 | 35 | // map from [0;1] to a cosine weighted distribution on a hemisphere centered around N = y 36 | // http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html 37 | vec3 sampleHemisphereCosine(vec2 Xi) 38 | { 39 | // turn Xi into spherical coordinates 40 | // azimuthal angle (360°) 41 | float phi = Xi.y * 2.0 * PI; 42 | // polar angle (90°) 43 | // 1-u to map the first sample (0) in the Hammersley sequence to 1=cos(0°)=up 44 | float cosTheta = sqrt(1.0 - Xi.x); 45 | float sinTheta = sqrt(1.0 - cosTheta * cosTheta); 46 | 47 | // to cartesian coordinates, y is up 48 | return vec3( 49 | cos(phi) * sinTheta, 50 | cosTheta, 51 | sin(phi) * sinTheta); 52 | } 53 | 54 | // importance sample the hemisphere for the GGX NDF instead 55 | // return value is the half-way vector H 56 | // a is perceptual roughness squared 57 | vec3 sampleGGX(vec2 Xi, float a) 58 | { 59 | float phi = 2.0 * PI * Xi.y; 60 | float cosTheta = sqrt((1.0 - Xi.x) / (1.0 + (a * a - 1.0) * Xi.x)); 61 | float sinTheta = sqrt(1.0 - cosTheta * cosTheta); 62 | 63 | return vec3( 64 | cos(phi) * sinTheta, 65 | cosTheta, 66 | sin(phi) * sinTheta); 67 | } 68 | 69 | #define IMPORTANCE_SAMPLE_BRDF 1 70 | 71 | // calculate the directional albedo = the irradiance reflected towards the eye position 72 | // from uniform lighting over the hemisphere 73 | float albedo_specular(vec3 V, float NoV, PBRMaterial mat) 74 | { 75 | // N points straight upwards (y) for this integration 76 | const vec3 N = vec3(0.0, 1.0, 0.0); 77 | 78 | float E = 0.0; 79 | 80 | // Monte-Carlo sampling for numerically integrating the directional albedo 81 | // E(V) = integral(brdf(V,L) * dot(N,L)) 82 | 83 | for(uint i = 0; i < NUM_SAMPLES; i++) 84 | { 85 | // quasirandom values in [0;1] 86 | // is there a better distribution we can use? 87 | // see http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/ 88 | vec2 Xi = hammersley_2d(i, NUM_SAMPLES); 89 | 90 | // sample microfacet direction 91 | // this returns H because we sample the NDF, which is the distribution of microfacets, which reflect around H 92 | // then the PDF needs the 4*VoH denominator since we evaluate L (related to the Jacobian) 93 | vec3 H = sampleGGX(Xi, mat.a); 94 | 95 | // get the light direction 96 | vec3 L = 2.0 * dot(V, H) * H - V; 97 | 98 | float NoL = saturate(dot(N, L)); 99 | float NoH = saturate(dot(N, H)); 100 | float VoH = saturate(dot(V, H)); 101 | 102 | // evaluate BRDF 103 | 104 | float F = F_Schlick(VoH, mat.F0).x; 105 | float VF = V_SmithGGXCorrelated(NoV, NoL, mat.a); 106 | 107 | //float Fr = F * VF * D; 108 | //float inv_pdf = (4.0 * VoH) / (D * NoH); 109 | //E += Fr * NoL * inv_pdf; 110 | // -> D cancels out 111 | 112 | E += F * VF * NoL * 4.0 * VoH / NoH; 113 | } 114 | 115 | return E / float(NUM_SAMPLES); 116 | } 117 | 118 | float albedo_diffuse(vec3 V, float NoV, PBRMaterial mat) 119 | { 120 | float E = 0.0; 121 | 122 | for(uint i = 0; i < NUM_SAMPLES; i++) 123 | { 124 | vec2 Xi = hammersley_2d(i, NUM_SAMPLES); 125 | 126 | vec3 L = sampleHemisphereCosine(Xi); 127 | vec3 H = normalize(V + L); 128 | 129 | float VoH = saturate(dot(V, H)); 130 | 131 | float F = F_Schlick(VoH, mat.F0).x; 132 | 133 | // Fr = (1 - F) * C * (1/pi) * NoL 134 | // float inv_pdf = pi/NoL 135 | // -> 1/pi and NoL cancel out 136 | 137 | E += (1.0 - F) * mat.diffuseColor.x; 138 | } 139 | 140 | return E / float(NUM_SAMPLES); 141 | } 142 | 143 | NUM_THREADS(THREADS, THREADS, 1) 144 | void main() 145 | { 146 | ivec2 coords = ivec2(gl_GlobalInvocationID.xy); 147 | vec2 values = (vec2(coords) + 0.5) / LUT_SIZE; 148 | 149 | float NoV = values.x; 150 | float roughness = sqrt(values.y); // LUT is indexed by a = roughness^2 151 | 152 | vec3 V; 153 | V.x = sqrt(1.0 - NoV * NoV); // sin 154 | V.y = NoV; // cos 155 | V.z = 0.0; 156 | 157 | PBRMaterial mat; 158 | // D3D compiler insists we initialize everything 159 | mat.normal = vec3(0.0, 1.0, 0.0); 160 | mat.occlusion = 0.0; 161 | mat.emissive = vec3_splat(0.0); 162 | mat.albedo = vec4_splat(1.0); 163 | mat.roughness = roughness; 164 | 165 | mat.metallic = 1.0; // F0 = albedo -> perfectly reflective surface 166 | mat = pbrInitMaterial(mat); 167 | float albedo_metal = albedo_specular(V, NoV, mat); 168 | 169 | mat.metallic = 0.0; // F0 = 0.04 170 | mat = pbrInitMaterial(mat); 171 | float albedo_dielectric = albedo_specular(V, NoV, mat) + albedo_diffuse(V, NoV, mat); 172 | 173 | imageStore(i_texAlbedoLUT, coords, vec4(albedo_metal, albedo_dielectric, 0.0, 1.0)); 174 | } 175 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/fs_clustered.sc: -------------------------------------------------------------------------------- 1 | $input v_worldpos, v_normal, v_tangent, v_texcoord0 2 | 3 | #define READ_MATERIAL 4 | 5 | #include 6 | #include 7 | #include "util.sh" 8 | #include "pbr.sh" 9 | #include "lights.sh" 10 | #include "clusters.sh" 11 | #include "colormap.sh" 12 | 13 | uniform vec4 u_camPos; 14 | 15 | void main() 16 | { 17 | // the clustered shading fragment shader is almost identical to forward shading 18 | // first we determine the cluster id from the fragment's window coordinates 19 | // light count is read from the grid instead of a uniform 20 | // light indices are read and looped over starting from the grid offset 21 | 22 | PBRMaterial mat = pbrMaterial(v_texcoord0); 23 | vec3 N = convertTangentNormal(v_normal, v_tangent, mat.normal); 24 | mat.a = specularAntiAliasing(N, mat.a); 25 | 26 | vec3 camPos = u_camPos.xyz; 27 | vec3 fragPos = v_worldpos; 28 | 29 | vec3 V = normalize(camPos - fragPos); 30 | float NoV = abs(dot(N, V)) + 1e-5; 31 | 32 | if(whiteFurnaceEnabled()) 33 | { 34 | mat.F0 = vec3_splat(1.0); 35 | vec3 msFactor = multipleScatteringFactor(mat, NoV); 36 | vec3 radianceOut = whiteFurnace(NoV, mat) * msFactor; 37 | gl_FragColor = vec4(radianceOut, 1.0); 38 | return; 39 | } 40 | 41 | vec3 msFactor = multipleScatteringFactor(mat, NoV); 42 | 43 | vec3 radianceOut = vec3_splat(0.0); 44 | 45 | uint cluster = getClusterIndex(gl_FragCoord); 46 | LightGrid grid = getLightGrid(cluster); 47 | for(uint i = 0; i < grid.pointLights; i++) 48 | { 49 | uint lightIndex = getGridLightIndex(grid.offset, i); 50 | PointLight light = getPointLight(lightIndex); 51 | float dist = distance(light.position, fragPos); 52 | float attenuation = smoothAttenuation(dist, light.radius); 53 | if(attenuation > 0.0) 54 | { 55 | vec3 L = normalize(light.position - fragPos); 56 | vec3 radianceIn = light.intensity * attenuation; 57 | float NoL = saturate(dot(N, L)); 58 | radianceOut += BRDF(V, L, N, NoV, NoL, mat) * msFactor * radianceIn * NoL; 59 | } 60 | } 61 | 62 | radianceOut += getAmbientLight().irradiance * mat.diffuseColor * mat.occlusion; 63 | radianceOut += mat.emissive; 64 | 65 | gl_FragColor.rgb = radianceOut; 66 | gl_FragColor.a = mat.albedo.a; 67 | } 68 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/fs_clustered_debug_vis.sc: -------------------------------------------------------------------------------- 1 | $input v_worldpos, v_normal, v_tangent, v_texcoord0 2 | 3 | #include 4 | #include "clusters.sh" 5 | #include "colormap.sh" 6 | 7 | void main() 8 | { 9 | // show light count per cluster 10 | 11 | uint cluster = getClusterIndex(gl_FragCoord); 12 | LightGrid grid = getLightGrid(cluster); 13 | 14 | int lights = int(grid.pointLights); 15 | // show possible clipping 16 | if(lights == 0) 17 | lights--; 18 | else if(lights == MAX_LIGHTS_PER_CLUSTER) 19 | lights++; 20 | 21 | vec3 lightCountColor = turboColormap(float(lights) / MAX_LIGHTS_PER_CLUSTER); 22 | gl_FragColor = vec4(lightCountColor, 1.0); 23 | 24 | // colorize clusters 25 | 26 | ARRAY_BEGIN(vec3, colors, 6) 27 | vec3(1.0, 0.0, 0.0), 28 | vec3(0.0, 1.0, 0.0), 29 | vec3(0.0, 0.0, 1.0), 30 | vec3(1.0, 1.0, 0.0), 31 | vec3(0.0, 1.0, 1.0), 32 | vec3(1.0, 0.0, 1.0) 33 | ARRAY_END(); 34 | 35 | //cluster = getClusterZIndex(gl_FragCoord.z); 36 | //vec3 sliceColor = colors[cluster % 6]; 37 | //gl_FragColor = vec4(sliceColor, 1.0); 38 | } 39 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/fs_deferred_fullscreen.sc: -------------------------------------------------------------------------------- 1 | #include 2 | #include "samplers.sh" 3 | #include "lights.sh" 4 | 5 | // G-Buffer 6 | SAMPLER2D(s_texDiffuseA, SAMPLER_DEFERRED_DIFFUSE_A); 7 | SAMPLER2D(s_texNormal, SAMPLER_DEFERRED_NORMAL); 8 | SAMPLER2D(s_texF0Metallic, SAMPLER_DEFERRED_F0_METALLIC); 9 | SAMPLER2D(s_texEmissiveOcclusion, SAMPLER_DEFERRED_EMISSIVE_OCCLUSION); 10 | SAMPLER2D(s_texDepth, SAMPLER_DEFERRED_DEPTH); 11 | 12 | void main() 13 | { 14 | vec2 texcoord = gl_FragCoord.xy / u_viewRect.zw; 15 | vec3 diffuseColor = texture2D(s_texDiffuseA, texcoord).xyz; 16 | vec4 emissiveOcclusion = texture2D(s_texEmissiveOcclusion, texcoord); 17 | vec3 emissive = emissiveOcclusion.xyz; 18 | float occlusion = emissiveOcclusion.w; 19 | 20 | vec3 radianceOut = vec3_splat(0.0); 21 | radianceOut += getAmbientLight().irradiance * diffuseColor * occlusion; 22 | radianceOut += emissive; 23 | 24 | gl_FragColor = vec4(radianceOut, 1.0); 25 | } 26 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/fs_deferred_geometry.sc: -------------------------------------------------------------------------------- 1 | $input v_normal, v_tangent, v_texcoord0 2 | 3 | #define READ_MATERIAL 4 | 5 | #include 6 | #include "util.sh" 7 | #include "pbr.sh" 8 | 9 | void main() 10 | { 11 | PBRMaterial mat = pbrMaterial(v_texcoord0); 12 | vec3 N = convertTangentNormal(v_normal, v_tangent, mat.normal); 13 | mat.a = specularAntiAliasing(N, mat.a); 14 | 15 | // save normal in camera space 16 | // the other renderers render in world space but the 17 | // deferred renderer uses camera space to hide artifacts from 18 | // normal packing and to make fragment position reconstruction easier 19 | // the normal matrix transforms to world coordinates, so undo that 20 | N = mul(u_view, vec4(N, 0.0)).xyz; 21 | 22 | // pack G-Buffer 23 | gl_FragData[0] = vec4(mat.diffuseColor, mat.a); 24 | gl_FragData[1] = vec4(packNormal(N), 0.0, 0.0); 25 | gl_FragData[2] = vec4(mat.F0, mat.metallic); 26 | gl_FragData[3] = vec4(mat.emissive, mat.occlusion); 27 | } 28 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/fs_deferred_pointlight.sc: -------------------------------------------------------------------------------- 1 | #include 2 | #include "samplers.sh" 3 | #include "pbr.sh" 4 | #include "lights.sh" 5 | #include "util.sh" 6 | 7 | // G-Buffer 8 | SAMPLER2D(s_texDiffuseA, SAMPLER_DEFERRED_DIFFUSE_A); 9 | SAMPLER2D(s_texNormal, SAMPLER_DEFERRED_NORMAL); 10 | SAMPLER2D(s_texF0Metallic, SAMPLER_DEFERRED_F0_METALLIC); 11 | SAMPLER2D(s_texDepth, SAMPLER_DEFERRED_DEPTH); 12 | 13 | uniform vec4 u_lightIndexVec; 14 | #define u_lightIndex uint(u_lightIndexVec.x) 15 | 16 | void main() 17 | { 18 | vec2 texcoord = gl_FragCoord.xy / u_viewRect.zw; 19 | 20 | vec4 diffuseA = texture2D(s_texDiffuseA, texcoord); 21 | vec3 N = unpackNormal(texture2D(s_texNormal, texcoord).xy); 22 | vec4 F0Metallic = texture2D(s_texF0Metallic, texcoord); 23 | 24 | // unpack material parameters used by the PBR BRDF function 25 | PBRMaterial mat; 26 | mat.diffuseColor = diffuseA.xyz; 27 | mat.a = diffuseA.w; 28 | mat.F0 = F0Metallic.xyz; 29 | mat.metallic = F0Metallic.w; 30 | 31 | // get fragment position 32 | // rendering happens in view space 33 | vec4 screen = gl_FragCoord; 34 | screen.z = texture2D(s_texDepth, texcoord).x; 35 | vec3 fragPos = screen2Eye(screen).xyz; 36 | 37 | // lighting 38 | 39 | vec3 radianceOut = vec3_splat(0.0); 40 | 41 | PointLight light = getPointLight(u_lightIndex); 42 | light.position = mul(u_view, vec4(light.position, 1.0)).xyz; 43 | 44 | float dist = distance(light.position, fragPos); 45 | float attenuation = smoothAttenuation(dist, light.radius); 46 | if(attenuation > 0.0) 47 | { 48 | vec3 V = normalize(-fragPos); 49 | float NoV = abs(dot(N, V)) + 1e-5; 50 | vec3 msFactor = multipleScatteringFactor(mat, NoV); 51 | 52 | vec3 L = normalize(light.position - fragPos); 53 | vec3 radianceIn = light.intensity * attenuation; 54 | float NoL = saturate(dot(N, L)); 55 | radianceOut += BRDF(V, L, N, NoV, NoL, mat) * msFactor * radianceIn * NoL; 56 | } 57 | 58 | gl_FragColor = vec4(radianceOut, 1.0); 59 | } 60 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/fs_forward.sc: -------------------------------------------------------------------------------- 1 | $input v_worldpos, v_normal, v_tangent, v_texcoord0 2 | 3 | // all unit-vectors need to be normalized in the fragment shader, the interpolation of vertex shader output doesn't preserve length 4 | 5 | // define samplers and uniforms for retrieving material parameters 6 | #define READ_MATERIAL 7 | 8 | #include 9 | #include 10 | #include "util.sh" 11 | #include "pbr.sh" 12 | #include "lights.sh" 13 | 14 | uniform vec4 u_camPos; 15 | 16 | void main() 17 | { 18 | PBRMaterial mat = pbrMaterial(v_texcoord0); 19 | // convert normal map from tangent space -> world space (= space of v_tangent, etc.) 20 | vec3 N = convertTangentNormal(v_normal, v_tangent, mat.normal); 21 | mat.a = specularAntiAliasing(N, mat.a); 22 | 23 | // shading 24 | 25 | vec3 camPos = u_camPos.xyz; 26 | vec3 fragPos = v_worldpos; 27 | 28 | vec3 V = normalize(camPos - fragPos); 29 | float NoV = abs(dot(N, V)) + 1e-5; 30 | 31 | if(whiteFurnaceEnabled()) 32 | { 33 | mat.F0 = vec3_splat(1.0); 34 | vec3 msFactor = multipleScatteringFactor(mat, NoV); 35 | vec3 radianceOut = whiteFurnace(NoV, mat) * msFactor; 36 | gl_FragColor = vec4(radianceOut, 1.0); 37 | return; 38 | } 39 | 40 | vec3 msFactor = multipleScatteringFactor(mat, NoV); 41 | 42 | vec3 radianceOut = vec3_splat(0.0); 43 | 44 | uint lights = pointLightCount(); 45 | for(uint i = 0; i < lights; i++) 46 | { 47 | PointLight light = getPointLight(i); 48 | float dist = distance(light.position, fragPos); 49 | float attenuation = smoothAttenuation(dist, light.radius); 50 | if(attenuation > 0.0) 51 | { 52 | vec3 L = normalize(light.position - fragPos); 53 | vec3 radianceIn = light.intensity * attenuation; 54 | float NoL = saturate(dot(N, L)); 55 | radianceOut += BRDF(V, L, N, NoV, NoL, mat) * msFactor * radianceIn * NoL; 56 | } 57 | } 58 | 59 | radianceOut += getAmbientLight().irradiance * mat.diffuseColor * mat.occlusion; 60 | radianceOut += mat.emissive; 61 | 62 | // output goes straight to HDR framebuffer, no clamping 63 | // tonemapping happens in final blit 64 | 65 | gl_FragColor.rgb = radianceOut; 66 | gl_FragColor.a = mat.albedo.a; 67 | } 68 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/fs_tonemap.sc: -------------------------------------------------------------------------------- 1 | #include 2 | #include "tonemapping.sh" 3 | 4 | uniform vec4 u_exposureVec; 5 | #define u_exposure u_exposureVec.x 6 | 7 | uniform vec4 u_tonemappingModeVec; 8 | #define u_tonemappingMode int(u_tonemappingModeVec.x) 9 | 10 | #define TONEMAP_NONE 0 11 | #define TONEMAP_EXPONENTIAL 1 12 | #define TONEMAP_REINHARD 2 13 | #define TONEMAP_REINHARD_LUM 3 14 | #define TONEMAP_HABLE 4 15 | #define TONEMAP_DUIKER 5 16 | #define TONEMAP_ACES 6 17 | #define TONEMAP_ACES_LUM 7 18 | 19 | SAMPLER2D(s_texColor, 0); 20 | 21 | void main() 22 | { 23 | vec2 texcoord = gl_FragCoord.xy / u_viewRect.zw; 24 | vec4 result = texture2D(s_texColor, texcoord); 25 | result.rgb *= u_exposure; 26 | 27 | switch(u_tonemappingMode) 28 | { 29 | default: 30 | case TONEMAP_NONE: 31 | result.rgb = saturate(result.rgb); 32 | break; 33 | case TONEMAP_EXPONENTIAL: 34 | result.rgb = tonemap_exponential(result.rgb); 35 | break; 36 | case TONEMAP_REINHARD: 37 | result.rgb = tonemap_reinhard(result.rgb); 38 | break; 39 | case TONEMAP_REINHARD_LUM: 40 | result.rgb = tonemap_reinhard_luminance(result.rgb); 41 | break; 42 | case TONEMAP_HABLE: 43 | result.rgb = tonemap_hable(result.rgb); 44 | break; 45 | case TONEMAP_DUIKER: 46 | result.rgb = tonemap_duiker(result.rgb); 47 | break; 48 | case TONEMAP_ACES: 49 | result.rgb = tonemap_aces(result.rgb); 50 | break; 51 | case TONEMAP_ACES_LUM: 52 | result.rgb = tonemap_aces_luminance(result.rgb); 53 | break; 54 | } 55 | 56 | gl_FragColor = result; 57 | } 58 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/lights.sh: -------------------------------------------------------------------------------- 1 | #ifndef LIGHTS_SH_HEADER_GUARD 2 | #define LIGHTS_SH_HEADER_GUARD 3 | 4 | #include 5 | #include "samplers.sh" 6 | 7 | uniform vec4 u_lightCountVec; 8 | #define u_pointLightCount uint(u_lightCountVec.x) 9 | 10 | uniform vec4 u_ambientLightIrradiance; 11 | 12 | // for each light: 13 | // vec4 position (w is padding) 14 | // vec4 intensity + radius (xyz is intensity, w is radius) 15 | BUFFER_RO(b_pointLights, vec4, SAMPLER_LIGHTS_POINTLIGHTS); 16 | 17 | struct PointLight 18 | { 19 | vec3 position; 20 | // this padding is necessary for Vulkan when using the struct in a shared workgroup array 21 | // otherwise memory reads/writes are corrupted 22 | // I can't find where this is required per the spec so I'll assume this is a bug with Nvidia drivers/HW 23 | float _padding; 24 | vec3 intensity; 25 | float radius; 26 | }; 27 | 28 | struct AmbientLight 29 | { 30 | vec3 irradiance; 31 | }; 32 | 33 | // primary source: 34 | // https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf 35 | // also really good: 36 | // https://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf 37 | 38 | float distanceAttenuation(float distance) 39 | { 40 | // only for point lights 41 | 42 | // physics: inverse square falloff 43 | // to keep irradiance from reaching infinity at really close distances, stop at 1cm 44 | return 1.0 / max(distance * distance, 0.01 * 0.01); 45 | } 46 | 47 | float smoothAttenuation(float distance, float radius) 48 | { 49 | // window function with smooth transition to 0 50 | // radius is arbitrary (and usually artist controlled) 51 | float nom = saturate(1.0 - pow(distance / radius, 4.0)); 52 | return nom * nom * distanceAttenuation(distance); 53 | } 54 | 55 | uint pointLightCount() 56 | { 57 | return u_pointLightCount; 58 | } 59 | 60 | PointLight getPointLight(uint i) 61 | { 62 | PointLight light; 63 | light.position = b_pointLights[2 * i + 0].xyz; 64 | vec4 intensityRadiusVec = b_pointLights[2 * i + 1]; 65 | light.intensity = intensityRadiusVec.xyz; 66 | light.radius = intensityRadiusVec.w; 67 | return light; 68 | } 69 | 70 | AmbientLight getAmbientLight() 71 | { 72 | AmbientLight light; 73 | light.irradiance = u_ambientLightIrradiance.xyz; 74 | return light; 75 | } 76 | 77 | #endif // LIGHTS_SH_HEADER_GUARD 78 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/pbr.sh: -------------------------------------------------------------------------------- 1 | #ifndef PBR_SH_HEADER_GUARD 2 | #define PBR_SH_HEADER_GUARD 3 | 4 | #include "samplers.sh" 5 | 6 | #ifdef WRITE_LUT 7 | IMAGE2D_WR(i_texAlbedoLUT, rgba32f, SAMPLER_PBR_ALBEDO_LUT); 8 | #else 9 | SAMPLER2D(s_texAlbedoLUT, SAMPLER_PBR_ALBEDO_LUT); 10 | #endif 11 | 12 | // only define this if you need to retrieve the material parameters 13 | // without it you can still use the struct definition or BRDF functions 14 | #ifdef READ_MATERIAL 15 | 16 | SAMPLER2D(s_texBaseColor, SAMPLER_PBR_BASECOLOR); 17 | SAMPLER2D(s_texMetallicRoughness, SAMPLER_PBR_METALROUGHNESS); 18 | SAMPLER2D(s_texNormal, SAMPLER_PBR_NORMAL); 19 | SAMPLER2D(s_texOcclusion, SAMPLER_PBR_OCCLUSION); 20 | SAMPLER2D(s_texEmissive, SAMPLER_PBR_EMISSIVE); 21 | 22 | uniform vec4 u_baseColorFactor; 23 | uniform vec4 u_metallicRoughnessNormalOcclusionFactor; 24 | uniform vec4 u_emissiveFactorVec; 25 | uniform vec4 u_hasTextures; 26 | 27 | #define u_hasBaseColorTexture ((uint(u_hasTextures.x) & (1 << 0)) != 0) 28 | #define u_hasMetallicRoughnessTexture ((uint(u_hasTextures.x) & (1 << 1)) != 0) 29 | #define u_hasNormalTexture ((uint(u_hasTextures.x) & (1 << 2)) != 0) 30 | #define u_hasOcclusionTexture ((uint(u_hasTextures.x) & (1 << 3)) != 0) 31 | #define u_hasEmissiveTexture ((uint(u_hasTextures.x) & (1 << 4)) != 0) 32 | 33 | #define u_metallicRoughnessFactor (u_metallicRoughnessNormalOcclusionFactor.xy) 34 | #define u_normalScale (u_metallicRoughnessNormalOcclusionFactor.z) 35 | #define u_occlusionStrength (u_metallicRoughnessNormalOcclusionFactor.w) 36 | #define u_emissiveFactor (u_emissiveFactorVec.xyz) 37 | 38 | #endif 39 | 40 | uniform vec4 u_multipleScatteringVec; 41 | #define u_multipleScattering (u_multipleScatteringVec.x != 0.0) 42 | #define u_whiteFurnaceRadiance (u_multipleScatteringVec.y) 43 | #define u_whiteFurnace (u_whiteFurnaceRadiance > 0.0) 44 | 45 | #define PI (3.14159265359) 46 | #define INV_PI (0.31830988618) 47 | 48 | struct PBRMaterial 49 | { 50 | vec4 albedo; 51 | float metallic; 52 | float roughness; 53 | vec3 normal; 54 | float occlusion; 55 | vec3 emissive; 56 | 57 | // calculated from the above 58 | 59 | vec3 diffuseColor; // this becomes black for higher metalness 60 | vec3 F0; // Fresnel reflectance at normal incidence 61 | float a; // remapped roughness (^2) 62 | }; 63 | 64 | #ifdef READ_MATERIAL 65 | 66 | vec4 pbrBaseColor(vec2 texcoord) 67 | { 68 | if(u_hasBaseColorTexture) 69 | { 70 | return texture2D(s_texBaseColor, texcoord) * u_baseColorFactor; 71 | } 72 | else 73 | { 74 | return u_baseColorFactor; 75 | } 76 | } 77 | 78 | vec2 pbrMetallicRoughness(vec2 texcoord) 79 | { 80 | if(u_hasMetallicRoughnessTexture) 81 | { 82 | return texture2D(s_texMetallicRoughness, texcoord).bg * u_metallicRoughnessFactor; 83 | } 84 | else 85 | { 86 | return u_metallicRoughnessFactor; 87 | } 88 | } 89 | 90 | vec3 pbrNormal(vec2 texcoord) 91 | { 92 | if(u_hasNormalTexture) 93 | { 94 | // the normal scale can cause problems and serves no real purpose 95 | // normal compression and BRDF calculations assume unit length 96 | return normalize((texture2D(s_texNormal, texcoord).rgb * 2.0) - 1.0); // * u_normalScale; 97 | } 98 | else 99 | { 100 | // the up vector (w) in tangent space is the default normal 101 | // that's why normal maps are mostly blue 102 | return vec3(0.0, 0.0, 1.0); 103 | } 104 | } 105 | 106 | float pbrOcclusion(vec2 texcoord) 107 | { 108 | if(u_hasOcclusionTexture) 109 | { 110 | // occludedColor = lerp(color, color * , ) 111 | float occlusion = texture2D(s_texOcclusion, texcoord).r; 112 | return occlusion + (1.0 - occlusion) * (1.0 - u_occlusionStrength); 113 | } 114 | else 115 | { 116 | return 1.0; 117 | } 118 | } 119 | 120 | vec3 pbrEmissive(vec2 texcoord) 121 | { 122 | if(u_hasEmissiveTexture) 123 | { 124 | return texture2D(s_texEmissive, texcoord).rgb * u_emissiveFactor; 125 | } 126 | else 127 | { 128 | return u_emissiveFactor; 129 | } 130 | } 131 | 132 | PBRMaterial pbrInitMaterial(PBRMaterial mat); 133 | 134 | PBRMaterial pbrMaterial(vec2 texcoord) 135 | { 136 | PBRMaterial mat; 137 | 138 | // Read textures/uniforms 139 | 140 | mat.albedo = pbrBaseColor(texcoord); 141 | vec2 metallicRoughness = pbrMetallicRoughness(texcoord); 142 | mat.metallic = metallicRoughness.r; 143 | mat.roughness = metallicRoughness.g; 144 | mat.normal = pbrNormal(texcoord); 145 | mat.occlusion = pbrOcclusion(texcoord); 146 | mat.emissive = pbrEmissive(texcoord); 147 | 148 | mat = pbrInitMaterial(mat); 149 | 150 | return mat; 151 | } 152 | 153 | #endif // READ_MATERIAL 154 | 155 | PBRMaterial pbrInitMaterial(PBRMaterial mat) 156 | { 157 | // Taken directly from GLTF 2.0 specs 158 | // this can be precalculated instead of evaluating it in the BRDF for every light 159 | 160 | const vec3 dielectricSpecular = vec3(0.04, 0.04, 0.04); 161 | const vec3 black = vec3(0.0, 0.0, 0.0); 162 | 163 | // metals have no diffuse reflection so the albedo value stores F0 (reflectance at normal incidence) instead 164 | // dielectrics are assumed to have F0 = 0.04 which equals an IOR of 1.5 165 | mat.diffuseColor = mix(mat.albedo.rgb * (vec3_splat(1.0) - dielectricSpecular), black, mat.metallic); 166 | mat.F0 = mix(dielectricSpecular, mat.albedo.rgb, mat.metallic); 167 | // perceptual roughness to roughness 168 | mat.a = mat.roughness * mat.roughness; 169 | // prevent division by 0 170 | mat.a = max(mat.a, 0.01); 171 | 172 | return mat; 173 | } 174 | 175 | // no screenspace derivatives in vertex or compute 176 | #if BGFX_SHADER_TYPE_FRAGMENT 177 | 178 | // Reduce specular aliasing by producing a modified roughness value 179 | 180 | // Tokuyoshi et al. 2019. Improved Geometric Specular Antialiasing. 181 | // http://www.jp.square-enix.com/tech/library/pdf/ImprovedGeometricSpecularAA.pdf 182 | float specularAntiAliasing(vec3 N, float a) 183 | { 184 | // normal-based isotropic filtering 185 | // this is originally meant for deferred rendering but is a bit simpler to implement than the forward version 186 | // saves us from calculating uv offsets and sampling textures for every light 187 | 188 | const float SIGMA2 = 0.25; // squared std dev of pixel filter kernel (in pixels) 189 | const float KAPPA = 0.18; // clamping threshold 190 | 191 | vec3 dndu = dFdx(N); 192 | vec3 dndv = dFdy(N); 193 | float variance = SIGMA2 * (dot(dndu, dndu) + dot(dndv, dndv)); 194 | float kernelRoughness2 = min(2.0 * variance, KAPPA); 195 | return saturate(a + kernelRoughness2); 196 | } 197 | 198 | #endif 199 | 200 | // Physically based shading 201 | // Metallic + roughness workflow (GLTF 2.0 core material spec) 202 | // BRDF, no sub-surface scattering 203 | // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material 204 | // Some GLSL code taken from 205 | // https://google.github.io/filament/Filament.md.html 206 | // and 207 | // https://learnopengl.com/PBR/Lighting 208 | 209 | // Schlick approximation to Fresnel equation 210 | vec3 F_Schlick(float VoH, vec3 F0) 211 | { 212 | float f = pow(1.0 - VoH, 5.0); 213 | return f + F0 * (1.0 - f); 214 | } 215 | 216 | // Normal Distribution Function 217 | // (aka specular distribution) 218 | // distribution of microfacets 219 | 220 | // Bruce Walter et al. 2007. Microfacet Models for Refraction through Rough Surfaces. 221 | // equivalent to Trowbridge-Reitz 222 | float D_GGX(float NoH, float a) 223 | { 224 | a = NoH * a; 225 | float k = a / (1.0 - NoH * NoH + a * a); 226 | return k * k * INV_PI; 227 | } 228 | 229 | // Visibility function 230 | // = Geometric Shadowing/Masking Function G, divided by the denominator of the Cook-Torrance BRDF (4 NoV NoL) 231 | // G is the probability of the microfacet being visible in the outgoing (masking) or incoming (shadowing) direction 232 | 233 | // Heitz 2014. Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs. 234 | // http://jcgt.org/published/0003/02/03/paper.pdf 235 | // based on height-correlated Smith-GGX 236 | float V_SmithGGXCorrelated(float NoV, float NoL, float a) 237 | { 238 | float a2 = a * a; 239 | float GGXV = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2); 240 | float GGXL = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2); 241 | return 0.5 / (GGXV + GGXL); 242 | } 243 | 244 | // version without height-correlation 245 | float V_SmithGGX(float NoV, float NoL, float a) 246 | { 247 | float a2 = a * a; 248 | float GGXV = NoV + sqrt(NoV * NoV * (1.0 - a2) + a2); 249 | float GGXL = NoL + sqrt(NoL * NoL * (1.0 - a2) + a2); 250 | return 1.0 / (GGXV * GGXL); 251 | } 252 | 253 | // Lambertian diffuse BRDF 254 | // uniform color 255 | float Fd_Lambert() 256 | { 257 | // normalize to conserve energy 258 | // cos integrates to pi over the hemisphere 259 | // incoming light is multiplied by cos and BRDF 260 | return INV_PI; 261 | } 262 | 263 | #ifndef WRITE_LUT 264 | 265 | // Account for multiple scattering across microfacets 266 | // Computes a scaling factor for the BRDF 267 | 268 | // Turquin. 2018. Practical multiple scattering compensation for microfacet models. 269 | // https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf 270 | vec3 multipleScatteringFactor(PBRMaterial mat, float NoV) 271 | { 272 | if(u_multipleScattering) 273 | { 274 | // Turquin approximates the multiple scattering portion of the BRDF using a scaled down version of the single scattering BRDF 275 | // That scale factor is E: the directional albedo for single scattering, ie. the total reflectance for a viewing direction 276 | vec2 E = texture2D(s_texAlbedoLUT, vec2(NoV, mat.a)).xy; 277 | 278 | // for metals, the albedo value is calculated with F = 1 (perfect reflection) 279 | // fresnel determines whether light is reflected or absorbed 280 | vec3 factorMetallic = vec3_splat(1.0) + mat.F0 * (1.0 / E.x - 1.0); 281 | 282 | // for dielectrics, fresnel determines the ratio between specular and diffuse energy 283 | // so the albedo depends on F as a variable 284 | // however, dielectrics in GLTF have a fixed F0 of 0.04 so we can do this with a second LUT 285 | vec3 factorDielectric = vec3_splat(1.0 / E.y); 286 | 287 | return mix(factorDielectric, factorMetallic, mat.metallic); 288 | } 289 | else 290 | return vec3_splat(1.0); 291 | } 292 | 293 | bool whiteFurnaceEnabled() 294 | { 295 | return u_whiteFurnace; 296 | } 297 | 298 | // White furnace test: lighting integral against a constant white environment 299 | // Used to visualize energy loss/gain 300 | // This is exactly what the multiple scattering LUT calculates 301 | vec3 whiteFurnace(float NoV, PBRMaterial mat) 302 | { 303 | vec2 Es = texture2D(s_texAlbedoLUT, vec2(NoV, mat.a)).xy; 304 | float E = mix(Es.y, Es.x, mat.metallic); 305 | return E * vec3_splat(u_whiteFurnaceRadiance); 306 | } 307 | 308 | #endif 309 | 310 | // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#appendix-b-brdf-implementation 311 | vec3 BRDF(vec3 v, vec3 l, vec3 n, float NoV, float NoL, PBRMaterial mat) 312 | { 313 | // V is the normalized vector from the shading location to the eye 314 | // L is the normalized vector from the shading location to the light 315 | // N is the surface normal in the same space as the above values 316 | // H is the half vector, where H = normalize(L+V) 317 | 318 | vec3 h = normalize(l + v); 319 | float NoH = saturate(dot(n, h)); 320 | float VoH = saturate(dot(v, h)); 321 | 322 | // specular BRDF 323 | float D = D_GGX(NoH, mat.a); 324 | vec3 F = F_Schlick(VoH, mat.F0); 325 | float V = V_SmithGGXCorrelated(NoV, NoL, mat.a); 326 | vec3 Fr = F * (V * D); 327 | 328 | // diffuse BRDF 329 | vec3 Fd = mat.diffuseColor * Fd_Lambert(); 330 | 331 | return Fr + (1.0 - F) * Fd; 332 | } 333 | 334 | #endif // PBR_SH_HEADER_GUARD 335 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/samplers.sh: -------------------------------------------------------------------------------- 1 | #ifndef SAMPLERS_SH_HEADER_GUARD 2 | #define SAMPLERS_SH_HEADER_GUARD 3 | 4 | // shared 5 | 6 | #define SAMPLER_PBR_ALBEDO_LUT 0 7 | 8 | #define SAMPLER_PBR_BASECOLOR 1 9 | #define SAMPLER_PBR_METALROUGHNESS 2 10 | #define SAMPLER_PBR_NORMAL 3 11 | #define SAMPLER_PBR_OCCLUSION 4 12 | #define SAMPLER_PBR_EMISSIVE 5 13 | 14 | #define SAMPLER_LIGHTS_POINTLIGHTS 6 15 | 16 | // per renderer 17 | 18 | #define SAMPLER_CLUSTERS_CLUSTERS 7 19 | #define SAMPLER_CLUSTERS_LIGHTINDICES 8 20 | #define SAMPLER_CLUSTERS_LIGHTGRID 9 21 | #define SAMPLER_CLUSTERS_ATOMICINDEX 10 22 | 23 | #define SAMPLER_DEFERRED_DIFFUSE_A 7 24 | #define SAMPLER_DEFERRED_NORMAL 8 25 | #define SAMPLER_DEFERRED_F0_METALLIC 9 26 | #define SAMPLER_DEFERRED_EMISSIVE_OCCLUSION 10 27 | #define SAMPLER_DEFERRED_DEPTH 11 28 | 29 | #endif // SAMPLERS_SH_HEADER_GUARD 30 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/tonemapping.sh: -------------------------------------------------------------------------------- 1 | #ifndef TONEMAPPING_SH_HEADER_GUARD 2 | #define TONEMAPPING_SH_HEADER_GUARD 3 | 4 | // relative luminance of linear RGB(!) 5 | // BT.709 primaries 6 | float luminance(vec3 RGB) 7 | { 8 | return 0.2126 * RGB.r + 0.7152 * RGB.g + 0.0722 * RGB.b; 9 | } 10 | 11 | // Reinhard, Hable, Duiken taken from: 12 | // http://filmicworlds.com/blog/filmic-tonemapping-operators/ 13 | 14 | // Exponential tone mapping 15 | vec3 tonemap_exponential(vec3 color) 16 | { 17 | return vec3_splat(1.0) - exp(-color); 18 | } 19 | 20 | // Reinhard et al 21 | // http://www.cs.utah.edu/~reinhard/cdrom/tonemap.pdf 22 | // simple version, desaturates colors 23 | 24 | vec3 tonemap_reinhard(vec3 color) 25 | { 26 | return color / (color + 1.0); 27 | } 28 | 29 | // Reinhard, luminance only 30 | // possibly creates undesirable whites 31 | // one alternative is to define a pure white point (ideally max luminance in the scene) 32 | // see original paper and https://imdoingitwrong.wordpress.com/2010/08/19/why-reinhard-desaturates-my-blacks-3/ 33 | 34 | vec3 tonemap_reinhard_luminance(vec3 color) 35 | { 36 | float lum = luminance(color); 37 | float nLum = lum / (lum + 1.0); 38 | return color * (nLum / lum); 39 | } 40 | 41 | // Uncharted 2 filmic operator 42 | // John Hable 43 | // https://www.gdcvault.com/play/1012351/Uncharted-2-HDR 44 | // https://www.slideshare.net/ozlael/hable-john-uncharted2-hdr-lighting 45 | 46 | vec3 hable_map(vec3 x) 47 | { 48 | // values used are directly from the presentation 49 | // comments have the values taken from the website above 50 | const float A = 0.22; // shoulder strength // 0.15 51 | const float B = 0.30; // linear strength // 0.50 52 | const float C = 0.10; // linear angle 53 | const float D = 0.20; // toe strength 54 | const float E = 0.01; // toe numerator // 0.02 55 | const float F = 0.30; // toe denominator 56 | return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F; 57 | } 58 | 59 | vec3 tonemap_hable(vec3 color) 60 | { 61 | //const float W = 11.2; // linear white point 62 | //vec3 whiteScale = hable_map(vec3_splat(W)); 63 | const float whiteScale = 0.72513; 64 | const float ExposureBias = 2.0; 65 | return hable_map(ExposureBias * color) / whiteScale; 66 | } 67 | 68 | // Mimics response curve of Kodak film 69 | // Haarm-Pieter Duiker 70 | // approximation by Hejl/Burgess-Dawson (pow 1/2.2 baked in) 71 | vec3 tonemap_duiker(vec3 color) 72 | { 73 | vec3 x = max(color - 0.004, 0.0); 74 | vec3 result = (x * (6.2 * x + 0.5)) / (x * (6.2 * x + 1.7) + 0.06); 75 | return pow(result, vec3_splat(2.2)); 76 | } 77 | 78 | // Polynomial fit of ACES 79 | // Stephen Hill 80 | // Taken from: 81 | // https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl 82 | vec3 tonemap_aces(vec3 color) 83 | { 84 | // sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT 85 | // sRGB refers to gamut, not display transform 86 | const mat3 ACESInputMat = mtxFromCols3( 87 | vec3(0.59719, 0.07600, 0.02840), 88 | vec3(0.35458, 0.90834, 0.13383), 89 | vec3(0.04823, 0.01566, 0.83777) 90 | ); 91 | 92 | // ODT_SAT => XYZ => D60_2_D65 => sRGB 93 | const mat3 ACESOutputMat = mtxFromCols3( 94 | vec3(1.60475, -0.10208, -0.00327), 95 | vec3(-0.53108, 1.10813, -0.07276), 96 | vec3(-0.07367, -0.00605, 1.07602) 97 | ); 98 | 99 | // colors in this code are premultiplied by 1.8 100 | // https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ToneMapping.hlsl 101 | // color *= 1.8; 102 | vec3 result = mul(ACESInputMat, color); 103 | 104 | // RRT and ODT 105 | vec3 v = result; 106 | vec3 a = v * (v + 0.0245786) - 0.000090537; 107 | vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081; 108 | color = a / b; 109 | 110 | result = mul(ACESOutputMat, color); 111 | return saturate(result); 112 | } 113 | 114 | // Luminance only fit of ACES 115 | // Oversatures brights similar to Reinhard in luminance 116 | // https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/ 117 | vec3 tonemap_aces_luminance(vec3 color) 118 | { 119 | const float a = 2.51; 120 | const float b = 0.03; 121 | const float c = 2.43; 122 | const float d = 0.59; 123 | const float e = 0.14; 124 | vec3 x = color * 0.6; 125 | return saturate((x * (a * x + b)) / (x * (c * x + d ) + e)); 126 | } 127 | 128 | #endif // TONEMAPPING_SH_HEADER_GUARD 129 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/util.sh: -------------------------------------------------------------------------------- 1 | #ifndef UTIL_SH_HEADER_GUARD 2 | #define UTIL_SH_HEADER_GUARD 3 | 4 | #include 5 | 6 | // from screen coordinates (gl_FragCoord) to eye space 7 | vec4 screen2Eye(vec4 coord) 8 | { 9 | #if BGFX_SHADER_LANGUAGE_GLSL 10 | // https://www.khronos.org/opengl/wiki/Compute_eye_space_from_window_space 11 | vec3 ndc = vec3( 12 | 2.0 * (coord.x - u_viewRect.x) / u_viewRect.z - 1.0, 13 | 2.0 * (coord.y - u_viewRect.y) / u_viewRect.w - 1.0, 14 | 2.0 * coord.z - 1.0 // -> [-1, 1] 15 | ); 16 | #else 17 | vec3 ndc = vec3( 18 | 2.0 * (coord.x - u_viewRect.x) / u_viewRect.z - 1.0, 19 | 2.0 * (u_viewRect.w - coord.y - 1 - u_viewRect.y) / u_viewRect.w - 1.0, // y is flipped 20 | coord.z // -> [0, 1] 21 | ); 22 | #endif 23 | 24 | // https://stackoverflow.com/a/16597492/862300 25 | vec4 eye = mul(u_invProj, vec4(ndc, 1.0)); 26 | eye = eye / eye.w; 27 | 28 | return eye; 29 | } 30 | 31 | // depth from screen coordinates (gl_FragCoord.z) to eye space 32 | // same as screen2Eye(vec4(0, 0, depth, 1)).z but slightly faster 33 | // (!) this assumes a perspective projection as created by bx::mtxProj 34 | // for a left-handed coordinate system 35 | float screen2EyeDepth(float depth, float near, float far) 36 | { 37 | // https://stackoverflow.com/a/45710371/862300 38 | 39 | #if BGFX_SHADER_LANGUAGE_GLSL 40 | float ndc = 2.0 * depth - 1.0; 41 | // ndc = (eye * (far + near) / (far - near) - 2 * (far * near) / (far - near)) / eye 42 | float eye = 2.0 * far * near / (far + near + ndc * (near - far)); 43 | #else 44 | float ndc = depth; 45 | // ndc = (eye * far / (far - near) - (far * near) / (far - near)) / eye 46 | float eye = far * near / (far + ndc * (near - far)); 47 | #endif 48 | 49 | return eye; 50 | } 51 | 52 | // convert normal from tangent space to space of normal_ref and tangent_ref 53 | vec3 convertTangentNormal(vec3 normal_ref, vec3 tangent_ref, vec3 normal) 54 | { 55 | vec3 bitangent = cross(normal_ref, tangent_ref); 56 | mat3 TBN = mtxFromCols( 57 | normalize(tangent_ref), 58 | normalize(bitangent), 59 | normalize(normal_ref) 60 | ); 61 | return normalize(mul(TBN, normal)); 62 | } 63 | 64 | // compress viewspace normal into two components 65 | // spheremap transform used by Cry Engine 3 66 | // https://aras-p.info/texts/CompactNormalStorage.html#method04spheremap 67 | // must be viewspace so we can hide z-pole 68 | // both (0,0,1) and (0,0,-1) have the same encoding 69 | // the original implementation always decoded to (0,0,1) 70 | // changed it to return normals pointing towards the camera for 71 | // a left-handed coordinate system 72 | 73 | vec2 packNormal(vec3 normal) 74 | { 75 | float f = sqrt(8.0 * -normal.z + 8.0); 76 | return normal.xy / f + 0.5; 77 | } 78 | 79 | vec3 unpackNormal(vec2 encoded) 80 | { 81 | vec2 fenc = encoded * 4.0 - 2.0; 82 | float f = dot(fenc, fenc); 83 | float g = sqrt(1.0 - f * 0.25); 84 | return vec3(fenc * g, -(1.0 - f * 0.5)); 85 | } 86 | 87 | #endif // UTIL_SH_HEADER_GUARD 88 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/varying.def.sc: -------------------------------------------------------------------------------- 1 | vec3 a_position : POSITION; 2 | vec3 a_normal : NORMAL; 3 | vec3 a_tangent : TANGENT; 4 | vec2 a_texcoord0 : TEXCOORD0; 5 | 6 | vec3 v_worldpos : POSITION1 = vec3(0.0, 0.0, 0.0); 7 | vec3 v_normal : NORMAL = vec3(0.0, 0.0, 0.0); 8 | vec3 v_tangent : TANGENT = vec3(0.0, 0.0, 0.0); 9 | vec2 v_texcoord0 : TEXCOORD0 = vec2(0.0, 0.0); 10 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/vs_clustered.sc: -------------------------------------------------------------------------------- 1 | $input a_position, a_normal, a_tangent, a_texcoord0 2 | $output v_worldpos, v_normal, v_tangent, v_texcoord0 3 | 4 | #include 5 | 6 | uniform mat3 u_normalMatrix; 7 | 8 | void main() 9 | { 10 | v_worldpos = mul(u_model[0], vec4(a_position, 1.0)).xyz; 11 | v_normal = mul(u_normalMatrix, a_normal); 12 | v_tangent = mul(u_model[0], vec4(a_tangent, 0.0)).xyz; 13 | v_texcoord0 = a_texcoord0; 14 | gl_Position = mul(u_modelViewProj, vec4(a_position, 1.0)); 15 | } 16 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/vs_deferred_fullscreen.sc: -------------------------------------------------------------------------------- 1 | $input a_position 2 | 3 | #include 4 | 5 | void main() 6 | { 7 | // fullscreen triangle is already transformed to clip space, nothing to do 8 | // set z to the far plane (= 1.0 in clip space) 9 | gl_Position = vec4(a_position.xy, 1.0, 1.0); 10 | } 11 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/vs_deferred_geometry.sc: -------------------------------------------------------------------------------- 1 | $input a_position, a_normal, a_tangent, a_texcoord0 2 | $output v_normal, v_tangent, v_texcoord0 3 | 4 | #include 5 | 6 | uniform mat3 u_normalMatrix; 7 | 8 | void main() 9 | { 10 | v_normal = mul(u_normalMatrix, a_normal); 11 | v_tangent = mul(u_model[0], vec4(a_tangent, 0.0)).xyz; 12 | v_texcoord0 = a_texcoord0; 13 | gl_Position = mul(u_modelViewProj, vec4(a_position, 1.0)); 14 | } 15 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/vs_deferred_light.sc: -------------------------------------------------------------------------------- 1 | $input a_position 2 | 3 | #include 4 | 5 | void main() 6 | { 7 | gl_Position = mul(u_modelViewProj, vec4(a_position, 1.0)); 8 | } 9 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/vs_forward.sc: -------------------------------------------------------------------------------- 1 | $input a_position, a_normal, a_tangent, a_texcoord0 2 | $output v_worldpos, v_normal, v_tangent, v_texcoord0 3 | 4 | #include 5 | 6 | // model transformation for normals to preserve perpendicularity 7 | // usually this is based on the model view matrix 8 | // but shading is done in world space 9 | uniform mat3 u_normalMatrix; 10 | 11 | void main() 12 | { 13 | v_worldpos = mul(u_model[0], vec4(a_position, 1.0)).xyz; 14 | v_normal = mul(u_normalMatrix, a_normal); 15 | v_tangent = mul(u_model[0], vec4(a_tangent, 0.0)).xyz; 16 | v_texcoord0 = a_texcoord0; 17 | gl_Position = mul(u_modelViewProj, vec4(a_position, 1.0)); 18 | } 19 | -------------------------------------------------------------------------------- /src/Renderer/Shaders/vs_tonemap.sc: -------------------------------------------------------------------------------- 1 | $input a_position 2 | 3 | #include 4 | 5 | void main() 6 | { 7 | gl_Position = vec4(a_position.xy, 0.0, 1.0); 8 | } 9 | -------------------------------------------------------------------------------- /src/Scene/Camera.cpp: -------------------------------------------------------------------------------- 1 | #include "Camera.h" 2 | 3 | #include 4 | #include 5 | 6 | const glm::vec3 Camera::X = { 1.0f, 0.0f, 0.0f }; 7 | const glm::vec3 Camera::Y = { 0.0f, 1.0f, 0.0f }; 8 | const glm::vec3 Camera::Z = { 0.0f, 0.0f, 1.0f }; 9 | 10 | void Camera::move(glm::vec3 delta) 11 | { 12 | pos += delta; 13 | } 14 | 15 | void Camera::rotate(glm::vec2 delta) 16 | { 17 | delta = glm::radians(delta); 18 | 19 | // limit pitch 20 | float dot = glm::dot(upAxis, forward()); 21 | if((dot < -0.99f && delta.x < 0.0f) || // angle nearing 180 degrees 22 | (dot > 0.99f && delta.x > 0.0f)) // angle nearing 0 degrees 23 | delta.x = 0.0f; 24 | 25 | // pitch is relative to current sideways rotation 26 | // yaw happens independently 27 | // this prevents roll 28 | rotation = glm::rotate(glm::identity(), delta.x, X) * // pitch 29 | rotation * glm::rotate(glm::identity(), delta.y, Y); // yaw 30 | // normalize? 31 | invRotation = glm::conjugate(rotation); 32 | } 33 | 34 | void Camera::zoom(float offset) 35 | { 36 | fov = glm::clamp(fov - offset, MIN_FOV, MAX_FOV); 37 | } 38 | 39 | void Camera::lookAt(const glm::vec3& position, const glm::vec3& target, const glm::vec3& up) 40 | { 41 | pos = position; 42 | upAxis = up; 43 | 44 | // model rotation 45 | // maps vectors to camera space (x, y, z) 46 | glm::vec3 forward = glm::normalize(target - position); 47 | rotation = glm::rotation(forward, Z); 48 | 49 | // correct the up vector 50 | // the cross product of non-orthogonal vectors is not normalized 51 | glm::vec3 right = glm::normalize(glm::cross(glm::normalize(up), forward)); // left-handed coordinate system 52 | glm::vec3 orthUp = glm::cross(forward, right); 53 | glm::quat upRotation = glm::rotation(rotation * orthUp, Y); 54 | rotation = upRotation * rotation; 55 | 56 | // inverse of the model rotation 57 | // maps camera space vectors to model vectors 58 | invRotation = glm::conjugate(rotation); 59 | } 60 | 61 | glm::vec3 Camera::position() const 62 | { 63 | return pos; 64 | } 65 | 66 | glm::mat4 Camera::matrix() const 67 | { 68 | return glm::toMat4(rotation) * glm::translate(glm::identity(), -pos); 69 | } 70 | 71 | glm::vec3 Camera::forward() const 72 | { 73 | return invRotation * Z; 74 | } 75 | 76 | glm::vec3 Camera::up() const 77 | { 78 | return invRotation * Y; 79 | } 80 | 81 | glm::vec3 Camera::right() const 82 | { 83 | return invRotation * X; 84 | } 85 | -------------------------------------------------------------------------------- /src/Scene/Camera.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | struct Camera 7 | { 8 | // fixed vertical field of view (Hor+) in degrees (full angle, not half) 9 | // default value is 90 degrees horizontal FOV on a 4:3 monitor 10 | // expands to 106.26 at 16:9 11 | // taken from Counter-Strike Global Offensive 12 | float fov = 73.7397953f; // degrees(atan(tan(radians(90)/2) / (4/3)) * 2) 13 | 14 | // the ratio zFar/zNear should be minimal 15 | // the higher it is, the more precision loss we get in the depth buffer (-> z-fighting) 16 | float zNear = 0.1f; // projection plane 17 | float zFar = 5.0f; // far plane 18 | 19 | // simply a multiplier for the radiance arriving at the camera 20 | float exposure = 1.0f; 21 | 22 | void move(glm::vec3 delta); // coordinates are in world-space (use forward/up/right to move relative to the camera) 23 | void rotate(glm::vec2 delta); // rotation around x, y axis 24 | void zoom(float offset); // > 0 = zoom in (decrease FOV by angles) 25 | 26 | void lookAt(const glm::vec3& position, const glm::vec3& target, const glm::vec3& up); 27 | 28 | glm::vec3 position() const; 29 | glm::mat4 matrix() const; 30 | 31 | // camera vectors in world-space coordinates 32 | glm::vec3 forward() const; 33 | glm::vec3 up() const; 34 | glm::vec3 right() const; 35 | 36 | private: 37 | static const glm::vec3 X, Y, Z; 38 | 39 | static constexpr float MIN_FOV = 10.0f; 40 | static constexpr float MAX_FOV = 90.0f; 41 | 42 | glm::vec3 upAxis = Y; 43 | 44 | glm::vec3 pos = { 0.0f, 0.0f, 0.0f }; 45 | glm::quat rotation = glm::identity(); 46 | glm::quat invRotation = glm::identity(); 47 | }; 48 | -------------------------------------------------------------------------------- /src/Scene/Light.cpp: -------------------------------------------------------------------------------- 1 | #include "Light.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | float PointLight::calculateRadius() const 8 | { 9 | // radius = where attenuation would lead to an intensity of 1W/m^2 10 | const float INTENSITY_CUTOFF = 1.0f; 11 | const float ATTENTUATION_CUTOFF = 0.05f; 12 | glm::vec3 intensity = flux / (4.0f * glm::pi()); 13 | float maxIntensity = glm::compMax(intensity); 14 | float attenuation = glm::max(INTENSITY_CUTOFF, ATTENTUATION_CUTOFF * maxIntensity) / maxIntensity; 15 | return 1.0f / sqrtf(attenuation); 16 | } 17 | -------------------------------------------------------------------------------- /src/Scene/Light.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | struct PointLight 7 | { 8 | glm::vec3 position; 9 | // radiant flux in W 10 | glm::vec3 flux; 11 | 12 | // calculate an appropriate radius for light culling 13 | // a windowing function in the shader will perform a smooth transition to zero 14 | // this is not physically based and usually artist controlled 15 | float calculateRadius() const; 16 | }; 17 | 18 | struct AmbientLight 19 | { 20 | glm::vec3 irradiance; 21 | }; 22 | -------------------------------------------------------------------------------- /src/Scene/LightList.cpp: -------------------------------------------------------------------------------- 1 | #include "LightList.h" 2 | 3 | #include 4 | #include 5 | 6 | bgfx::VertexLayout LightList::PointLightVertex::layout; 7 | 8 | void PointLightList::init() 9 | { 10 | LightList::PointLightVertex::init(); 11 | buffer = bgfx::createDynamicVertexBuffer( 12 | 1, PointLightVertex::layout, BGFX_BUFFER_COMPUTE_READ | BGFX_BUFFER_ALLOW_RESIZE); 13 | } 14 | 15 | void PointLightList::shutdown() 16 | { 17 | bgfx::destroy(buffer); 18 | buffer = BGFX_INVALID_HANDLE; 19 | } 20 | 21 | void PointLightList::update() 22 | { 23 | size_t stride = PointLightVertex::layout.getStride(); 24 | const bgfx::Memory* mem = bgfx::alloc(uint32_t(stride * std::max(lights.size(), (size_t)1))); 25 | 26 | for(size_t i = 0; i < lights.size(); i++) 27 | { 28 | PointLightVertex* light = (PointLightVertex*)(mem->data + (i * stride)); 29 | light->position = lights[i].position; 30 | // intensity = flux per unit solid angle (steradian) 31 | // there are 4*pi steradians in a sphere 32 | light->intensity = lights[i].flux / (4.0f * glm::pi()); 33 | light->radius = lights[i].calculateRadius(); 34 | } 35 | 36 | bgfx::update(buffer, 0, mem); 37 | } 38 | -------------------------------------------------------------------------------- /src/Scene/LightList.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Scene/Light.h" 4 | #include 5 | #include 6 | 7 | struct LightList 8 | { 9 | // vertex buffers seem to be aligned to 16 bytes 10 | struct PointLightVertex 11 | { 12 | glm::vec3 position; 13 | float padding; 14 | // radiant intensity in W/sr 15 | // can be calculated from radiant flux 16 | glm::vec3 intensity; 17 | float radius; 18 | 19 | static void init() 20 | { 21 | layout.begin() 22 | .add(bgfx::Attrib::TexCoord0, 4, bgfx::AttribType::Float) 23 | .add(bgfx::Attrib::TexCoord1, 4, bgfx::AttribType::Float) 24 | .end(); 25 | } 26 | static bgfx::VertexLayout layout; 27 | }; 28 | }; 29 | 30 | class PointLightList : public LightList 31 | { 32 | public: 33 | void init(); 34 | void shutdown(); 35 | 36 | // upload changes to GPU 37 | void update(); 38 | 39 | std::vector lights; 40 | 41 | bgfx::DynamicVertexBufferHandle buffer = BGFX_INVALID_HANDLE; 42 | }; 43 | -------------------------------------------------------------------------------- /src/Scene/Material.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material 8 | struct Material 9 | { 10 | bool blend = false; 11 | bool doubleSided = false; 12 | 13 | bgfx::TextureHandle baseColorTexture = BGFX_INVALID_HANDLE; 14 | glm::vec4 baseColorFactor = { 1.0f, 1.0f, 1.0f, 1.0f }; 15 | 16 | bgfx::TextureHandle metallicRoughnessTexture = BGFX_INVALID_HANDLE; // blue = metallic, green = roughness 17 | float metallicFactor = 1.0f; 18 | float roughnessFactor = 1.0f; 19 | 20 | bgfx::TextureHandle normalTexture = BGFX_INVALID_HANDLE; 21 | float normalScale = 1.0f; 22 | 23 | bgfx::TextureHandle occlusionTexture = BGFX_INVALID_HANDLE; 24 | float occlusionStrength = 1.0f; 25 | 26 | bgfx::TextureHandle emissiveTexture = BGFX_INVALID_HANDLE; 27 | glm::vec3 emissiveFactor = { 0.0f, 0.0f, 0.0f }; 28 | }; 29 | -------------------------------------------------------------------------------- /src/Scene/Mesh.cpp: -------------------------------------------------------------------------------- 1 | #include "Mesh.h" 2 | 3 | // initialized in Scene::init 4 | bgfx::VertexLayout Mesh::PosNormalTangentTex0Vertex::layout; 5 | -------------------------------------------------------------------------------- /src/Scene/Mesh.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct Mesh 6 | { 7 | bgfx::VertexBufferHandle vertexBuffer = BGFX_INVALID_HANDLE; 8 | bgfx::IndexBufferHandle indexBuffer = BGFX_INVALID_HANDLE; 9 | unsigned int material = 0; // index into materials vector 10 | 11 | //bgfx::OcclusionQueryHandle occlusionQuery = BGFX_INVALID_HANDLE; 12 | 13 | // bgfx vertex attributes 14 | // initialized by Scene 15 | struct PosNormalTangentTex0Vertex 16 | { 17 | float x, y, z; // position 18 | float nx, ny, nz; // normal 19 | float tx, ty, tz; // tangent 20 | float u, v; // UV coordinates 21 | 22 | static void init() 23 | { 24 | layout.begin() 25 | .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float) 26 | .add(bgfx::Attrib::Normal, 3, bgfx::AttribType::Float) 27 | .add(bgfx::Attrib::Tangent, 3, bgfx::AttribType::Float) 28 | .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float) 29 | .end(); 30 | } 31 | static bgfx::VertexLayout layout; 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/Scene/Scene.cpp: -------------------------------------------------------------------------------- 1 | #include "Scene.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | bx::DefaultAllocator Scene::allocator; 18 | 19 | Scene::Scene() : 20 | skyColor({ 0.53f, 0.81f, 0.98f }), // https://en.wikipedia.org/wiki/Sky_blue#Light_sky_blue 21 | ambientLight({ { 0.03f, 0.03f, 0.03f } }) 22 | { 23 | clear(); 24 | Assimp::DefaultLogger::set(&logSource); 25 | } 26 | 27 | void Scene::init() 28 | { 29 | Mesh::PosNormalTangentTex0Vertex::init(); 30 | } 31 | 32 | void Scene::clear() 33 | { 34 | if(loaded) 35 | { 36 | for(Mesh& mesh : meshes) 37 | { 38 | bgfx::destroy(mesh.vertexBuffer); 39 | bgfx::destroy(mesh.indexBuffer); 40 | mesh.vertexBuffer = BGFX_INVALID_HANDLE; 41 | mesh.indexBuffer = BGFX_INVALID_HANDLE; 42 | } 43 | 44 | for(Material& mat : materials) 45 | { 46 | if(bgfx::isValid(mat.baseColorTexture)) 47 | { 48 | bgfx::destroy(mat.baseColorTexture); 49 | mat.baseColorTexture = BGFX_INVALID_HANDLE; 50 | } 51 | if(bgfx::isValid(mat.metallicRoughnessTexture)) 52 | { 53 | bgfx::destroy(mat.metallicRoughnessTexture); 54 | mat.metallicRoughnessTexture = BGFX_INVALID_HANDLE; 55 | } 56 | if(bgfx::isValid(mat.normalTexture)) 57 | { 58 | bgfx::destroy(mat.normalTexture); 59 | mat.normalTexture = BGFX_INVALID_HANDLE; 60 | } 61 | if(bgfx::isValid(mat.occlusionTexture)) 62 | { 63 | bgfx::destroy(mat.occlusionTexture); 64 | mat.occlusionTexture = BGFX_INVALID_HANDLE; 65 | } 66 | if(bgfx::isValid(mat.emissiveTexture)) 67 | { 68 | bgfx::destroy(mat.emissiveTexture); 69 | mat.emissiveTexture = BGFX_INVALID_HANDLE; 70 | } 71 | } 72 | 73 | meshes.clear(); 74 | materials.clear(); 75 | pointLights.shutdown(); 76 | pointLights.lights.clear(); 77 | } 78 | minBounds = maxBounds = { 0.0f, 0.0f, 0.0f }; 79 | center = { 0.0f, 0.0f, 0.0f }; 80 | diagonal = 0.0f; 81 | camera = Camera(); 82 | loaded = false; 83 | } 84 | 85 | bool Scene::load(const char* file) 86 | { 87 | clear(); 88 | 89 | pointLights.init(); 90 | 91 | Assimp::Importer importer; 92 | 93 | // Settings for aiProcess_SortByPType 94 | // only take triangles or higher (polygons are triangulated during import) 95 | importer.SetPropertyInteger(AI_CONFIG_PP_SBP_REMOVE, aiPrimitiveType_LINE | aiPrimitiveType_POINT); 96 | // Settings for aiProcess_SplitLargeMeshes 97 | // Limit vertices to 65k (we use 16-bit indices) 98 | importer.SetPropertyInteger(AI_CONFIG_PP_SLM_VERTEX_LIMIT, std::numeric_limits::max()); 99 | 100 | unsigned int flags = 101 | aiProcessPreset_TargetRealtime_Quality | // some optimizations and safety checks 102 | aiProcess_OptimizeMeshes | // minimize number of meshes 103 | aiProcess_PreTransformVertices | // apply node matrices 104 | aiProcess_FixInfacingNormals | aiProcess_TransformUVCoords | // apply UV transformations 105 | //aiProcess_FlipWindingOrder | // we cull clock-wise, keep the default CCW winding order 106 | aiProcess_MakeLeftHanded | // we set GLM_FORCE_LEFT_HANDED and use left-handed bx matrix functions 107 | aiProcess_FlipUVs; // bimg loads textures with flipped Y (top left is 0,0) 108 | 109 | const aiScene* scene = nullptr; 110 | try 111 | { 112 | scene = importer.ReadFile(file, flags); 113 | } 114 | catch(const std::exception& e) 115 | { 116 | Log->error("{}", e.what()); 117 | } 118 | 119 | if(scene) 120 | { 121 | if(!(scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE)) 122 | { 123 | for(unsigned int i = 0; i < scene->mNumMeshes; i++) 124 | { 125 | try 126 | { 127 | meshes.push_back(loadMesh(scene->mMeshes[i])); 128 | } 129 | catch(std::exception& e) 130 | { 131 | Log->warn("{}", e.what()); 132 | } 133 | } 134 | 135 | center = minBounds + (maxBounds - minBounds) / 2.0f; 136 | glm::vec3 extent = glm::abs(maxBounds - minBounds); 137 | diagonal = glm::sqrt(glm::dot(extent, extent)); 138 | 139 | char dir[bx::kMaxFilePath] = ""; 140 | bx::strCopy(dir, BX_COUNTOF(dir), bx::FilePath(file).getPath()); 141 | for(unsigned int i = 0; i < scene->mNumMaterials; i++) 142 | { 143 | try 144 | { 145 | materials.push_back(loadMaterial(scene->mMaterials[i], dir)); 146 | } 147 | catch(std::exception& e) 148 | { 149 | // material not loaded, use default 150 | // really only happens if there is no diffuse color 151 | materials.push_back(Material()); 152 | Log->warn("{}", e.what()); 153 | } 154 | } 155 | 156 | // bring opaque meshes to the front so alpha blending works 157 | // still need depth sorting for scenes with overlapping transparent meshes 158 | std::partition( 159 | meshes.begin(), meshes.end(), [this](const Mesh& mesh) { return !materials[mesh.material].blend; }); 160 | 161 | if(scene->HasCameras()) 162 | { 163 | camera = loadCamera(scene->mCameras[0]); 164 | } 165 | else 166 | { 167 | Log->info("No camera"); 168 | camera.lookAt(center - glm::vec3(0.0f, 0.0f, diagonal / 2.0f), center, glm::vec3(0.0f, 1.0f, 0.0f)); 169 | camera.zFar = diagonal; 170 | camera.zNear = camera.zFar / 50.0f; 171 | } 172 | 173 | loaded = true; 174 | } 175 | else 176 | Log->error("Scene is incomplete or invalid"); 177 | } 178 | 179 | return loaded; 180 | } 181 | 182 | Mesh Scene::loadMesh(const aiMesh* mesh) 183 | { 184 | if(mesh->mPrimitiveTypes != aiPrimitiveType_TRIANGLE) 185 | throw std::runtime_error("Mesh has incompatible primitive type"); 186 | 187 | if(mesh->mNumVertices > (std::numeric_limits::max() + 1u)) 188 | throw std::runtime_error("Mesh has too many vertices (> uint16_t::max + 1)"); 189 | 190 | constexpr size_t coords = 0; 191 | bool hasTexture = mesh->mNumUVComponents[coords] == 2 && mesh->mTextureCoords[coords] != nullptr; 192 | 193 | // vertices 194 | 195 | uint32_t stride = Mesh::PosNormalTangentTex0Vertex::layout.getStride(); 196 | 197 | const bgfx::Memory* vertexMem = bgfx::alloc(mesh->mNumVertices * stride); 198 | 199 | for(unsigned int i = 0; i < mesh->mNumVertices; i++) 200 | { 201 | unsigned int offset = i * stride; 202 | Mesh::PosNormalTangentTex0Vertex& vertex = *(Mesh::PosNormalTangentTex0Vertex*)(vertexMem->data + offset); 203 | 204 | aiVector3D pos = mesh->mVertices[i]; 205 | vertex.x = pos.x; 206 | vertex.y = pos.y; 207 | vertex.z = pos.z; 208 | 209 | minBounds = glm::min(minBounds, { pos.x, pos.y, pos.z }); 210 | maxBounds = glm::max(maxBounds, { pos.x, pos.y, pos.z }); 211 | 212 | aiVector3D nrm = mesh->mNormals[i]; 213 | vertex.nx = nrm.x; 214 | vertex.ny = nrm.y; 215 | vertex.nz = nrm.z; 216 | 217 | aiVector3D tan = mesh->mTangents[i]; 218 | vertex.tx = tan.x; 219 | vertex.ty = tan.y; 220 | vertex.tz = tan.z; 221 | 222 | if(hasTexture) 223 | { 224 | aiVector3D uv = mesh->mTextureCoords[coords][i]; 225 | vertex.u = uv.x; 226 | vertex.v = uv.y; 227 | } 228 | } 229 | 230 | bgfx::VertexBufferHandle vbh = bgfx::createVertexBuffer(vertexMem, Mesh::PosNormalTangentTex0Vertex::layout); 231 | 232 | // indices (triangles) 233 | 234 | const bgfx::Memory* iMem = bgfx::alloc(mesh->mNumFaces * 3 * sizeof(uint16_t)); 235 | uint16_t* indices = (uint16_t*)iMem->data; 236 | 237 | for(unsigned int i = 0; i < mesh->mNumFaces; i++) 238 | { 239 | assert(mesh->mFaces[i].mNumIndices == 3); 240 | indices[(3 * i) + 0] = (uint16_t)mesh->mFaces[i].mIndices[0]; 241 | indices[(3 * i) + 1] = (uint16_t)mesh->mFaces[i].mIndices[1]; 242 | indices[(3 * i) + 2] = (uint16_t)mesh->mFaces[i].mIndices[2]; 243 | } 244 | 245 | bgfx::IndexBufferHandle ibh = bgfx::createIndexBuffer(iMem); 246 | 247 | return { vbh, ibh, mesh->mMaterialIndex }; 248 | } 249 | 250 | Material Scene::loadMaterial(const aiMaterial* material, const char* dir) 251 | { 252 | Material out; 253 | 254 | // technically there is a difference between MASK and BLEND mode 255 | // but for our purposes it's enough if we sort properly 256 | aiString alphaMode; 257 | material->Get(AI_MATKEY_GLTF_ALPHAMODE, alphaMode); 258 | aiString alphaModeOpaque; 259 | alphaModeOpaque.Set("OPAQUE"); 260 | out.blend = alphaMode != alphaModeOpaque; 261 | 262 | material->Get(AI_MATKEY_TWOSIDED, out.doubleSided); 263 | 264 | // texture files 265 | 266 | aiString fileBaseColor, fileMetallicRoughness, fileNormals, fileOcclusion, fileEmissive; 267 | material->GetTexture(AI_MATKEY_BASE_COLOR_TEXTURE, &fileBaseColor); 268 | // TODO AI_MATKEY_METALLIC_TEXTURE + AI_MATKEY_ROUGHNESS_TEXTURE 269 | material->GetTexture(AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE, &fileMetallicRoughness); 270 | material->GetTexture(aiTextureType_NORMALS, 0, &fileNormals); 271 | // TODO aiTextureType_AMBIENT_OCCLUSION, what's the difference? 272 | material->GetTexture(aiTextureType_LIGHTMAP, 0, &fileOcclusion); 273 | material->GetTexture(aiTextureType_EMISSIVE, 0, &fileEmissive); 274 | 275 | // diffuse 276 | 277 | if(fileBaseColor.length > 0) 278 | { 279 | aiString pathBaseColor; 280 | pathBaseColor.Set(dir); 281 | pathBaseColor.Append(fileBaseColor.C_Str()); 282 | out.baseColorTexture = loadTexture(pathBaseColor.C_Str(), true /* sRGB */); 283 | } 284 | 285 | aiColor4D baseColorFactor; 286 | if(AI_SUCCESS == material->Get(AI_MATKEY_BASE_COLOR, baseColorFactor)) 287 | out.baseColorFactor = { baseColorFactor.r, baseColorFactor.g, baseColorFactor.b, baseColorFactor.a }; 288 | out.baseColorFactor = glm::clamp(out.baseColorFactor, 0.0f, 1.0f); 289 | 290 | // metallic/roughness 291 | 292 | if(fileMetallicRoughness.length > 0) 293 | { 294 | aiString pathMetallicRoughness; 295 | pathMetallicRoughness.Set(dir); 296 | pathMetallicRoughness.Append(fileMetallicRoughness.C_Str()); 297 | out.metallicRoughnessTexture = loadTexture(pathMetallicRoughness.C_Str()); 298 | } 299 | 300 | ai_real metallicFactor; 301 | if(AI_SUCCESS == material->Get(AI_MATKEY_METALLIC_FACTOR, metallicFactor)) 302 | out.metallicFactor = glm::clamp(metallicFactor, 0.0f, 1.0f); 303 | ai_real roughnessFactor; 304 | if(AI_SUCCESS == material->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughnessFactor)) 305 | out.roughnessFactor = glm::clamp(roughnessFactor, 0.0f, 1.0f); 306 | 307 | // normal map 308 | 309 | if(fileNormals.length > 0) 310 | { 311 | aiString pathNormals; 312 | pathNormals.Set(dir); 313 | pathNormals.Append(fileNormals.C_Str()); 314 | out.normalTexture = loadTexture(pathNormals.C_Str()); 315 | } 316 | 317 | ai_real normalScale; 318 | if(AI_SUCCESS == material->Get(AI_MATKEY_GLTF_TEXTURE_SCALE(aiTextureType_NORMALS, 0), normalScale)) 319 | out.normalScale = normalScale; 320 | 321 | // occlusion texture 322 | 323 | if(fileOcclusion == fileMetallicRoughness) 324 | { 325 | // some GLTF files combine metallic/roughness and occlusion values into one texture 326 | // don't load it twice 327 | out.occlusionTexture = out.metallicRoughnessTexture; 328 | } 329 | else if(fileOcclusion.length > 0) 330 | { 331 | aiString pathOcclusion; 332 | pathOcclusion.Set(dir); 333 | pathOcclusion.Append(fileOcclusion.C_Str()); 334 | out.occlusionTexture = loadTexture(pathOcclusion.C_Str()); 335 | } 336 | 337 | ai_real occlusionStrength; 338 | if(AI_SUCCESS == material->Get(AI_MATKEY_GLTF_TEXTURE_STRENGTH(aiTextureType_LIGHTMAP, 0), occlusionStrength)) 339 | out.occlusionStrength = glm::clamp(occlusionStrength, 0.0f, 1.0f); 340 | 341 | // emissive texture 342 | 343 | if(fileEmissive.length > 0) 344 | { 345 | aiString pathEmissive; 346 | pathEmissive.Set(dir); 347 | pathEmissive.Append(fileEmissive.C_Str()); 348 | out.emissiveTexture = loadTexture(pathEmissive.C_Str(), true /* sRGB */); 349 | } 350 | 351 | aiColor3D emissiveFactor; 352 | if(AI_SUCCESS == material->Get(AI_MATKEY_COLOR_EMISSIVE, emissiveFactor)) 353 | out.emissiveFactor = { emissiveFactor.r, emissiveFactor.g, emissiveFactor.b }; 354 | out.emissiveFactor = glm::clamp(out.emissiveFactor, 0.0f, 1.0f); 355 | 356 | return out; 357 | } 358 | 359 | Camera Scene::loadCamera(const aiCamera* camera) 360 | { 361 | float aspect = camera->mAspect == 0.0f ? 16.0f / 9.0f : camera->mAspect; 362 | glm::vec3 pos(camera->mPosition.x, camera->mPosition.y, camera->mPosition.z); 363 | glm::vec3 target(camera->mLookAt.x, camera->mLookAt.y, camera->mLookAt.z); 364 | glm::vec3 up(camera->mUp.x, camera->mUp.y, camera->mUp.z); 365 | 366 | Camera cam; 367 | cam.lookAt(pos, target, up); 368 | // convert horizontal half angle (radians) to vertical full angle (degrees) 369 | cam.fov = glm::degrees(2.0f * glm::atan(glm::tan(camera->mHorizontalFOV) / aspect)); 370 | cam.zNear = camera->mClipPlaneNear; 371 | cam.zFar = camera->mClipPlaneFar; 372 | 373 | return cam; 374 | } 375 | 376 | bgfx::TextureHandle Scene::loadTexture(const char* file, bool sRGB) 377 | { 378 | void* data = nullptr; 379 | uint32_t size = 0; 380 | 381 | bx::FileReader reader; 382 | bx::Error err; 383 | if(bx::open(&reader, file, &err)) 384 | { 385 | size = (uint32_t)bx::getSize(&reader); 386 | data = BX_ALLOC(&allocator, size); 387 | bx::read(&reader, data, size, &err); 388 | bx::close(&reader); 389 | } 390 | 391 | if(!err.isOk()) 392 | { 393 | BX_FREE(&allocator, data); 394 | throw std::runtime_error(err.getMessage().getPtr()); 395 | } 396 | 397 | bimg::ImageContainer* image = bimg::imageParse(&allocator, data, size); 398 | if(image) 399 | { 400 | // the callback gets called when bgfx is done using the data (after 2 frames) 401 | const bgfx::Memory* mem = bgfx::makeRef( 402 | image->m_data, 403 | image->m_size, 404 | [](void*, void* data) { bimg::imageFree((bimg::ImageContainer*)data); }, 405 | image); 406 | BX_FREE(&allocator, data); 407 | 408 | // default wrap mode is repeat, there's no flag for it 409 | uint64_t textureFlags = BGFX_TEXTURE_NONE | BGFX_SAMPLER_MIN_ANISOTROPIC | BGFX_SAMPLER_MAG_ANISOTROPIC; 410 | if(sRGB) 411 | textureFlags |= BGFX_TEXTURE_SRGB; 412 | 413 | if(bgfx::isTextureValid(0, false, image->m_numLayers, (bgfx::TextureFormat::Enum)image->m_format, textureFlags)) 414 | { 415 | bgfx::TextureHandle tex = bgfx::createTexture2D((uint16_t)image->m_width, 416 | (uint16_t)image->m_height, 417 | image->m_numMips > 1, 418 | image->m_numLayers, 419 | (bgfx::TextureFormat::Enum)image->m_format, 420 | textureFlags, 421 | mem); 422 | //bgfx::setName(tex, file); // causes debug errors with DirectX SetPrivateProperty duplicate 423 | return tex; 424 | } 425 | else 426 | throw std::runtime_error("Unsupported image format"); 427 | } 428 | 429 | BX_FREE(&allocator, data); 430 | throw std::runtime_error(err.getMessage().getPtr()); 431 | } 432 | -------------------------------------------------------------------------------- /src/Scene/Scene.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Scene/Camera.h" 4 | #include "Scene/Mesh.h" 5 | #include "Scene/Material.h" 6 | #include "Scene/Light.h" 7 | #include "Scene/LightList.h" 8 | #include "Log/AssimpSource.h" 9 | #include 10 | #include 11 | #include 12 | 13 | struct aiMesh; 14 | struct aiMaterial; 15 | struct aiCamera; 16 | 17 | class Scene 18 | { 19 | public: 20 | Scene(); 21 | 22 | static void init(); 23 | 24 | // load meshes, materials, camera from .gltf file 25 | bool load(const char* file); 26 | void clear(); 27 | 28 | bool loaded = false; 29 | glm::vec3 minBounds; 30 | glm::vec3 maxBounds; 31 | glm::vec3 center; 32 | float diagonal; 33 | Camera camera; 34 | std::vector meshes; 35 | std::vector materials; 36 | 37 | // these are not populated by load 38 | glm::vec3 skyColor; 39 | AmbientLight ambientLight; 40 | PointLightList pointLights; 41 | 42 | private: 43 | static bx::DefaultAllocator allocator; 44 | AssimpLogSource logSource; 45 | 46 | Mesh loadMesh(const aiMesh* mesh); // not static because it changes minBounds and maxBounds 47 | static Material loadMaterial(const aiMaterial* material, const char* dir); 48 | static Camera loadCamera(const aiCamera* camera); 49 | 50 | static bgfx::TextureHandle loadTexture(const char* file, bool sRGB = false); 51 | }; 52 | -------------------------------------------------------------------------------- /src/UI.cpp: -------------------------------------------------------------------------------- 1 | #include "UI.h" 2 | 3 | #include "Cluster.h" 4 | #include "Scene/Scene.h" 5 | #include "Config.h" 6 | #include "Renderer/Renderer.h" 7 | #include "Log/UISink.h" 8 | #include "Log/Log.h" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | using namespace std::placeholders; 17 | 18 | ClusterUI::ClusterUI(Cluster& app) : app(app) { } 19 | 20 | void ClusterUI::initialize() 21 | { 22 | // Log 23 | 24 | auto func = std::bind(&ClusterUI::log, this, std::placeholders::_1, std::placeholders::_2); 25 | logUISink = std::make_shared>(func); 26 | logUISink->set_level(spdlog::level::trace); 27 | logUISink->set_pattern("%v"); 28 | Sinks->add_sink(logUISink); 29 | 30 | // Imgui 31 | 32 | ImGuiIO& io = ImGui::GetIO(); 33 | //io.IniFilename = nullptr; // don't save window positions etc. to ini 34 | //io.MouseDrawCursor = true; // let imgui draw cursors 35 | 36 | ImGuiStyle& style = ImGui::GetStyle(); 37 | ImGui::StyleColorsDark(&style); 38 | 39 | // convert imgui style colors to linear RGB 40 | // since we have an sRGB backbuffer 41 | for(size_t i = 0; i < ImGuiCol_COUNT; i++) 42 | { 43 | glm::vec3 sRGB = glm::make_vec3(&style.Colors[i].x); 44 | glm::vec3 linear = glm::convertSRGBToLinear(sRGB); 45 | style.Colors[i] = ImVec4(linear.x, linear.y, linear.z, style.Colors[i].w); 46 | } 47 | 48 | // no round corners 49 | style.WindowRounding = 0.0f; 50 | style.ChildRounding = 0.0f; 51 | style.FrameRounding = 0.0f; 52 | style.GrabRounding = 0.0f; 53 | style.PopupRounding = 0.0f; 54 | style.ScrollbarRounding = 0.0f; 55 | 56 | // align text to the left 57 | // looks better with icon buttons 58 | style.ButtonTextAlign = ImVec2(0.0f, 0.5f); 59 | 60 | // Load text font 61 | io.Fonts->Clear(); 62 | const char* fontFile = "assets/fonts/Roboto/Roboto-Medium.ttf"; 63 | ImFontConfig fontConfig; 64 | fontConfig.GlyphRanges = io.Fonts->GetGlyphRangesDefault(); // basic + extended Latin only 65 | ImFont* font = io.Fonts->AddFontFromFileTTF(fontFile, 14.0f, &fontConfig); 66 | if(!font) 67 | io.Fonts->AddFontDefault(); 68 | 69 | // Load and merge icon font 70 | const char* iconFontFile = "assets/fonts/ForkAwesome/forkawesome-webfont.ttf"; 71 | static const ImWchar iconsRanges[] = { ICON_MIN_FK, ICON_MAX_16_FK, 0 }; // must persist for font lifetime 72 | ImFontConfig iconsConfig; 73 | iconsConfig.MergeMode = true; 74 | iconsConfig.GlyphRanges = iconsRanges; 75 | iconsConfig.GlyphMinAdvanceX = 13.0f; // align icons 76 | iconsConfig.PixelSnapH = true; 77 | iconsConfig.OversampleH = 1; 78 | iconsConfig.OversampleV = 1; 79 | io.Fonts->AddFontFromFileTTF(iconFontFile, 13.0f, &iconsConfig); 80 | 81 | // Generate font texture 82 | unsigned char* tex_data = nullptr; 83 | int tex_w = 0, tex_h = 0; 84 | int bytes = 0; 85 | io.Fonts->GetTexDataAsRGBA32(&tex_data, &tex_w, &tex_h, &bytes); 86 | fontTexture = bgfx::createTexture2D((uint16_t)tex_w, 87 | (uint16_t)tex_h, 88 | false, 89 | 1, 90 | bgfx::TextureFormat::RGBA8, 91 | 0, 92 | bgfx::copy(tex_data, tex_w * tex_h * bytes)); 93 | io.Fonts->SetTexID((ImTextureID)(uintptr_t)fontTexture.idx); 94 | } 95 | 96 | void ClusterUI::update(float dt) 97 | { 98 | mTime += dt; 99 | if(!app.config->showUI) 100 | return; 101 | 102 | //ImGui::ShowDemoWindow(); 103 | 104 | const Renderer::TextureBuffer* buffers = app.renderer->buffers; 105 | const ImVec2 padding = { 5.0f, 5.0f }; 106 | 107 | if(app.config->showConfigWindow) 108 | { 109 | ImGui::Begin("Settings", &app.config->showConfigWindow, ImGuiWindowFlags_AlwaysAutoResize); 110 | 111 | if(ImGui::SliderInt("No. of lights", &app.config->lights, 0, app.config->maxLights)) 112 | app.generateLights(app.config->lights); 113 | ImGui::Checkbox("Moving lights", &app.config->movingLights); 114 | 115 | ImGui::Separator(); 116 | 117 | ImGui::SliderFloat("Exposure", &app.scene->camera.exposure, 0.0f, 30.0f, "%.3f"); 118 | 119 | const char* operators[] = { "None", 120 | "Exponential", 121 | "Reinhard", 122 | "Reinhard - Luminance", 123 | "Hable (Uncharted 2)", 124 | "Duiker (Kodak curve)", 125 | "ACES", 126 | "ACES - Luminance" }; 127 | int tonemappingMode = (int)app.config->tonemappingMode; 128 | ImGui::Combo("Tonemapping", &tonemappingMode, operators, IM_ARRAYSIZE(operators)); 129 | app.config->tonemappingMode = (Renderer::TonemappingMode)tonemappingMode; 130 | app.renderer->setTonemappingMode(app.config->tonemappingMode); 131 | 132 | ImGui::Separator(); 133 | 134 | ImGui::Checkbox("Multiple scattering", &app.config->multipleScattering); 135 | app.renderer->setMultipleScattering(app.config->multipleScattering); 136 | 137 | ImGui::Checkbox("White furnace", &app.config->whiteFurnace); 138 | ImGui::SameLine(); 139 | ImGui::Text(ICON_FK_INFO_CIRCLE); 140 | if(ImGui::IsItemHovered()) 141 | ImGui::SetTooltip("Not implemented in the deferred renderer"); 142 | app.renderer->setWhiteFurnace(app.config->whiteFurnace); 143 | 144 | ImGui::Separator(); 145 | 146 | ImGui::Text("Render path:"); 147 | static int renderPathSelected = (int)app.config->renderPath; 148 | ImGui::RadioButton("Forward", &renderPathSelected, (int)Cluster::RenderPath::Forward); 149 | ImGui::RadioButton("Deferred", &renderPathSelected, (int)Cluster::RenderPath::Deferred); 150 | ImGui::RadioButton("Clustered", &renderPathSelected, (int)Cluster::RenderPath::Clustered); 151 | Cluster::RenderPath path = (Cluster::RenderPath)renderPathSelected; 152 | if(path != app.config->renderPath) 153 | app.setRenderPath(path); 154 | 155 | ImGui::Separator(); 156 | 157 | ImGui::Checkbox("Show log", &app.config->showLog); 158 | ImGui::Checkbox("Show performance stats", &app.config->showStatsOverlay); 159 | if(buffers) 160 | ImGui::Checkbox("Show G-Buffer", &app.config->showBuffers); 161 | if(path == Cluster::RenderPath::Clustered) 162 | { 163 | ImGui::Checkbox("Cluster light count visualization", &app.config->debugVisualization); 164 | app.renderer->setVariable("DEBUG_VIS", app.config->debugVisualization ? "true" : "false"); 165 | } 166 | 167 | ImGui::Separator(); 168 | 169 | if(ImGui::Button(app.config->fullscreen ? (ICON_FK_WINDOW_RESTORE " Restore") 170 | : (ICON_FK_WINDOW_MAXIMIZE " Fullscreen"), 171 | ImVec2(100, 0))) 172 | { 173 | app.toggleFullscreen(); 174 | } 175 | if(ImGui::Button(ICON_FK_EYE_SLASH " Hide UI", ImVec2(100, 0))) 176 | app.config->showUI = false; 177 | ImGui::SameLine(); 178 | std::string keyName = glfwGetKeyName(GLFW_KEY_R, 0); 179 | for(auto& c : keyName) 180 | c = std::toupper(c); 181 | ImGui::TextDisabled("%s to restore", keyName.c_str()); 182 | ImGui::End(); 183 | } 184 | 185 | // log 186 | if(app.config->showLog) 187 | { 188 | ImGui::Begin("Log", &app.config->showLog, ImGuiWindowFlags_HorizontalScrollbar); 189 | ImVec4 clrText = ImGui::GetStyleColorVec4(ImGuiCol_Text); 190 | ImVec4 clrDisabled = ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled); 191 | ImVec4 clrWarning = ImVec4(1.0f, 0.5f, 0.0f, 1.0f); // yellow/orange ("Gold") 192 | ImVec4 clrError = ImVec4(0.8f, 0.0f, 0.1f, 1.0f); // yellow/orange ("Crimson") 193 | // trace, debug, info, warn, error, critical 194 | const ImVec4 colors[] = { clrDisabled, clrDisabled, clrText, clrWarning, clrError, clrError }; 195 | const char* icons[] = { ICON_FK_INFO, ICON_FK_INFO, ICON_FK_INFO, 196 | ICON_FK_EXCLAMATION, ICON_FK_EXCLAMATION, ICON_FK_EXCLAMATION }; 197 | for(const LogEntry& entry : logEntries) 198 | { 199 | ImGui::TextColored(colors[entry.level], "%s %s", icons[entry.level], logText.begin() + entry.messageOffset); 200 | } 201 | // only scroll down if it's currently at the bottom 202 | // TODO this breaks scrolling up 203 | //if(ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) 204 | // ImGui::SetScrollHereY(1.0f); 205 | ImGui::End(); 206 | } 207 | 208 | // performance overlay 209 | if(app.config->showStatsOverlay) 210 | { 211 | const float overlayWidth = 150.0f; 212 | 213 | // top left, transparent background 214 | ImGui::SetNextWindowPos(padding, ImGuiCond_Always); 215 | ImGui::SetNextWindowBgAlpha(0.5f); 216 | ImGui::Begin("Stats", 217 | nullptr, 218 | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoBringToFrontOnFocus | 219 | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | 220 | ImGuiWindowFlags_NoNav); 221 | 222 | // title 223 | ImGui::Text(ICON_FK_TACHOMETER " Stats"); 224 | ImGui::TextDisabled("right-click for options"); 225 | ImGui::Separator(); 226 | 227 | // general data 228 | const bgfx::Stats* stats = bgfx::getStats(); 229 | const double toCpuMs = 1000.0 / double(stats->cpuTimerFreq); 230 | const double toGpuMs = 1000.0 / double(stats->gpuTimerFreq); 231 | 232 | ImGui::Text("Backend: %s", bgfx::getRendererName(bgfx::getRendererType())); 233 | ImGui::Text("Buffer size: %u x %u px", stats->width, stats->height); 234 | ImGui::Text("Triangles: %u", stats->numPrims[bgfx::Topology::TriList]); 235 | ImGui::Text("Draw calls: %u", stats->numDraw); 236 | ImGui::Text("Compute calls: %u", stats->numCompute); 237 | 238 | // plots 239 | static float fpsValues[GRAPH_HISTORY] = { 0 }; 240 | static float frameTimeValues[GRAPH_HISTORY] = { 0 }; 241 | static float gpuMemoryValues[GRAPH_HISTORY] = { 0 }; 242 | static size_t offset = 0; 243 | 244 | if(app.config->overlays.fps) 245 | { 246 | ImGui::Separator(); 247 | ImGui::Text("FPS"); 248 | ImGui::PlotLines("", 249 | fpsValues, 250 | IM_ARRAYSIZE(fpsValues), 251 | (int)offset + 1, 252 | nullptr, 253 | 0.0f, 254 | 200.0f, 255 | ImVec2(overlayWidth, 50)); 256 | ImGui::Text("%.0f", fpsValues[offset]); 257 | } 258 | if(app.config->overlays.frameTime) 259 | { 260 | ImGui::Separator(); 261 | ImGui::Text("Frame time"); 262 | ImGui::PlotLines("", 263 | frameTimeValues, 264 | IM_ARRAYSIZE(frameTimeValues), 265 | (int)offset + 1, 266 | nullptr, 267 | 0.0f, 268 | 30.0f, 269 | ImVec2(overlayWidth, 50)); 270 | ImGui::Text("CPU: %.2f ms", float(stats->cpuTimeEnd - stats->cpuTimeBegin) * toCpuMs); 271 | ImGui::Text("GPU: %.2f ms", float(stats->gpuTimeEnd - stats->gpuTimeBegin) * toGpuMs); 272 | ImGui::Text("Total: %.2f ms", frameTimeValues[offset]); 273 | } 274 | if(app.config->profile && app.config->overlays.profiler) 275 | { 276 | ImGui::Separator(); 277 | ImGui::Text("View stats"); 278 | if(stats->numViews > 0) 279 | { 280 | ImVec4 cpuColor(0.5f, 1.0f, 0.5f, 1.0f); 281 | ImVec4 gpuColor(0.5f, 0.5f, 1.0f, 1.0f); 282 | 283 | const float itemHeight = ImGui::GetTextLineHeightWithSpacing(); 284 | const float itemHeightWithSpacing = ImGui::GetFrameHeightWithSpacing(); 285 | const float scale = 2.0f; 286 | 287 | if(ImGui::BeginListBox("##ViewStats", ImVec2(overlayWidth, stats->numViews * itemHeightWithSpacing))) 288 | { 289 | ImGuiListClipper clipper; 290 | clipper.Begin(stats->numViews, itemHeight); 291 | 292 | while(clipper.Step()) 293 | { 294 | for(int32_t pos = clipper.DisplayStart; pos < clipper.DisplayEnd; ++pos) 295 | { 296 | const bgfx::ViewStats& viewStats = stats->viewStats[pos]; 297 | float cpuElapsed = float((viewStats.cpuTimeEnd - viewStats.cpuTimeBegin) * toCpuMs); 298 | float gpuElapsed = float((viewStats.gpuTimeEnd - viewStats.gpuTimeBegin) * toGpuMs); 299 | 300 | ImGui::Text("%d", viewStats.view); 301 | 302 | const float maxWidth = overlayWidth * 0.35f; 303 | const float cpuWidth = bx::clamp(cpuElapsed * scale, 1.0f, maxWidth); 304 | const float gpuWidth = bx::clamp(gpuElapsed * scale, 1.0f, maxWidth); 305 | 306 | ImGui::SameLine(overlayWidth * 0.3f); 307 | 308 | if(drawBar("CPU", cpuWidth, maxWidth, itemHeight, cpuColor)) 309 | { 310 | ImGui::SetTooltip("%s -- CPU: %.2f ms", viewStats.name, cpuElapsed); 311 | } 312 | 313 | ImGui::SameLine(); 314 | 315 | if(drawBar("GPU", gpuWidth, maxWidth, itemHeight, gpuColor)) 316 | { 317 | ImGui::SetTooltip("%s -- GPU: %.2f ms", viewStats.name, gpuElapsed); 318 | } 319 | } 320 | } 321 | 322 | clipper.End(); 323 | 324 | ImGui::EndListBox(); 325 | } 326 | } 327 | else 328 | { 329 | ImGui::TextWrapped(ICON_FK_EXCLAMATION_TRIANGLE " Profiler disabled"); 330 | } 331 | } 332 | if(app.config->overlays.gpuMemory) 333 | { 334 | int64_t used = stats->gpuMemoryUsed; 335 | int64_t max = stats->gpuMemoryMax; 336 | 337 | ImGui::Separator(); 338 | if(used > 0 && max > 0) 339 | { 340 | ImGui::Text("GPU memory"); 341 | ImGui::PlotLines("", 342 | gpuMemoryValues, 343 | IM_ARRAYSIZE(gpuMemoryValues), 344 | (int)offset + 1, 345 | nullptr, 346 | 0.0f, 347 | float(max), 348 | ImVec2(overlayWidth, 50)); 349 | char strUsed[64]; 350 | bx::prettify(strUsed, BX_COUNTOF(strUsed), stats->gpuMemoryUsed); 351 | char strMax[64]; 352 | bx::prettify(strMax, BX_COUNTOF(strMax), stats->gpuMemoryMax); 353 | ImGui::Text("%s / %s", strUsed, strMax); 354 | } 355 | else 356 | { 357 | ImGui::TextWrapped(ICON_FK_EXCLAMATION_TRIANGLE " GPU memory data unavailable"); 358 | } 359 | } 360 | 361 | // update after drawing so offset is the current value 362 | static float oldTime = 0.0f; 363 | if(mTime - oldTime > GRAPH_FREQUENCY) 364 | { 365 | offset = (offset + 1) % GRAPH_HISTORY; 366 | ImGuiIO& io = ImGui::GetIO(); 367 | fpsValues[offset] = 1 / io.DeltaTime; 368 | frameTimeValues[offset] = io.DeltaTime * 1000; 369 | gpuMemoryValues[offset] = float(stats->gpuMemoryUsed) / 1024 / 1024; 370 | 371 | oldTime = mTime; 372 | } 373 | 374 | // right click menu 375 | if(ImGui::BeginPopupContextWindow()) 376 | { 377 | ImGui::Checkbox("FPS", &app.config->overlays.fps); 378 | ImGui::Checkbox("Frame time", &app.config->overlays.frameTime); 379 | if(app.config->profile) 380 | ImGui::Checkbox("View stats", &app.config->overlays.profiler); 381 | ImGui::Checkbox("GPU memory", &app.config->overlays.gpuMemory); 382 | ImGui::EndPopup(); 383 | } 384 | ImGui::End(); 385 | } 386 | 387 | if(buffers && app.config->showBuffers) 388 | { 389 | ImGui::SetNextWindowBgAlpha(0.5f); 390 | ImGui::Begin("Buffers", 391 | nullptr, 392 | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoBringToFrontOnFocus | 393 | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | 394 | ImGuiWindowFlags_NoNav); 395 | 396 | ImGuiIO& io = ImGui::GetIO(); 397 | 398 | ImVec4 tintColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); 399 | ImVec4 borderColor = ImVec4(1.0f, 1.0f, 1.0f, 0.5f); 400 | 401 | for(size_t i = 0; buffers[i].name != nullptr; i++) 402 | { 403 | ImGui::Text("%s", buffers[i].name); 404 | ImTextureID texId = ImTextureID(uintptr_t(buffers[i].handle.idx)); 405 | ImVec2 texSize = io.DisplaySize; 406 | texSize = { 128.0f, 128.0f }; 407 | ImVec2 topLeft = ImVec2(0.0f, 0.0f); 408 | ImVec2 bottomRight = ImVec2(1.0f, 1.0f); 409 | if(bgfx::getCaps()->originBottomLeft) 410 | { 411 | std::swap(topLeft.y, bottomRight.y); 412 | } 413 | ImGui::Image(texId, texSize, topLeft, bottomRight, tintColor, borderColor); 414 | } 415 | 416 | // move window to bottom right 417 | ImVec2 displaySize = io.DisplaySize; 418 | ImVec2 windowSize = ImGui::GetWindowSize(); 419 | ImVec2 windowPos(displaySize.x - windowSize.x - padding.x, displaySize.y - windowSize.y - padding.y); 420 | ImGui::SetWindowPos(windowPos); 421 | 422 | ImGui::End(); 423 | } 424 | } 425 | 426 | void ClusterUI::shutdown() 427 | { 428 | logEntries.clear(); 429 | logText.clear(); 430 | ImGuiIO& io = ImGui::GetIO(); 431 | // destroy font texture since we always create it ourselves 432 | bgfx::destroy(fontTexture); 433 | fontTexture = BGFX_INVALID_HANDLE; 434 | io.Fonts->SetTexID(ImTextureID(uintptr_t(fontTexture.idx))); 435 | Sinks->remove_sink(logUISink); 436 | logUISink = nullptr; 437 | } 438 | 439 | void ClusterUI::log(const char* message, spdlog::level::level_enum level) 440 | { 441 | // TODO limit to 2000 entries 442 | // rolling index? 443 | int vecLen = logText.size(); 444 | int32_t strLen = bx::strLen(message) + 1; 445 | logText.resize(vecLen + strLen); 446 | bx::memCopy(logText.begin() + vecLen, message, strLen); 447 | 448 | logEntries.push_back({ level, vecLen }); 449 | } 450 | 451 | bool ClusterUI::drawBar(const char* id, float width, float maxWidth, float height, const ImVec4& color) 452 | { 453 | const ImGuiStyle& style = ImGui::GetStyle(); 454 | 455 | ImVec4 hoveredColor(color.x * 1.1f, color.y * 1.1f, color.z * 1.1f, color.w * 1.1f); 456 | 457 | ImGui::PushStyleColor(ImGuiCol_Button, color); 458 | ImGui::PushStyleColor(ImGuiCol_ButtonHovered, hoveredColor); 459 | ImGui::PushStyleColor(ImGuiCol_ButtonActive, color); 460 | ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); 461 | ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, style.ItemSpacing.y)); 462 | 463 | bool itemHovered = false; 464 | 465 | ImGui::PushID(id); 466 | 467 | ImGui::Button("##Visible", ImVec2(width, height)); 468 | itemHovered = itemHovered || ImGui::IsItemHovered(); 469 | 470 | ImGui::SameLine(); 471 | ImGui::InvisibleButton("##Invisible", ImVec2(std::max(1.0f, maxWidth - width), height)); 472 | itemHovered = itemHovered || ImGui::IsItemHovered(); 473 | 474 | ImGui::PopID(); 475 | 476 | ImGui::PopStyleVar(2); 477 | ImGui::PopStyleColor(3); 478 | 479 | return itemHovered; 480 | } 481 | -------------------------------------------------------------------------------- /src/UI.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | class Cluster; 8 | 9 | class ClusterUI 10 | { 11 | public: 12 | ClusterUI(Cluster& app); 13 | 14 | void initialize(); 15 | void update(float dt); 16 | void shutdown(); 17 | 18 | void log(const char* message, spdlog::level::level_enum level = spdlog::level::info); 19 | 20 | private: 21 | bool drawBar(const char* id, float width, float maxWidth, float height, const ImVec4& color); 22 | 23 | spdlog::sink_ptr logUISink = nullptr; 24 | 25 | struct LogEntry 26 | { 27 | spdlog::level::level_enum level; 28 | int messageOffset; // points into vector of char 29 | }; 30 | 31 | ImVector logEntries; 32 | ImVector logText; 33 | 34 | // update 10 times per second 35 | static constexpr float GRAPH_FREQUENCY = 0.1f; 36 | // show 100 values 37 | static constexpr size_t GRAPH_HISTORY = 100; 38 | 39 | Cluster& app; 40 | float mTime = 0.0f; 41 | 42 | bgfx::TextureHandle fontTexture = BGFX_INVALID_HANDLE; 43 | }; 44 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "Cluster.h" 2 | 3 | int main(int argc, char* argv[]) 4 | { 5 | Cluster app; 6 | return app.run(argc, argv); 7 | } 8 | --------------------------------------------------------------------------------