From c1beec87ff7965487dbc658d0e123bfef1ca656a Mon Sep 17 00:00:00 2001 From: =?utf8?q?Jean-Philippe=20Bruy=C3=A8re?= Date: Tue, 10 Sep 2019 03:15:01 +0200 Subject: [PATCH 1/1] split vk.net and cvkl, cvkl renamed in vke.net --- .gitignore | 7 + .travis.yml | 12 + Directory.Build.props | 14 + LICENSE.md | 21 + README.md | 96 ++ SpirVTasks/CompileGLSLTask.cs | 203 ++++ SpirVTasks/README.md | 62 ++ SpirVTasks/SpirVTasks.csproj | 39 + SpirVTasks/SpirVTasks.targets | 30 + SpirVTasks/spirv.xml | 9 + addons/Directory.Build.props | 45 + addons/DistanceFieldFont/BMChar.cs | 49 + addons/DistanceFieldFont/BMFont.cs | 207 ++++ .../DistanceFieldFont.csproj | 20 + addons/VkvgPipeline/VkvgPipeline.cs | 135 +++ addons/VkvgPipeline/VkvgPipeline.csproj | 23 + addons/gltfLoader/PbrModel.cs | 124 +++ addons/gltfLoader/PbrModelSeparateTextures.cs | 162 +++ addons/gltfLoader/PbrModelTexArray.cs | 139 +++ addons/gltfLoader/glTFLoader.cs | 635 +++++++++++ addons/gltfLoader/gltfLoader.csproj | 21 + clean.sh | 2 + netfx.props | 27 + samples/Directory.Build.props | 44 + .../DistanceFieldFontTest.csproj | 9 + samples/DistanceFieldFontTest/Program.cs | 300 ++++++ .../DistanceFieldFontTest/shaders/main.frag | 32 + .../DistanceFieldFontTest/shaders/main.vert | 28 + samples/Model/Model.csproj | 7 + samples/Model/data/DamagedHelmet.bin | Bin 0 -> 558504 bytes samples/Model/data/DamagedHelmet.gltf | 3 + samples/Model/main.cs | 257 +++++ samples/Model/shaders/model.frag | 84 ++ samples/Model/shaders/model.vert | 50 + samples/Textured/Textured.csproj | 8 + samples/Textured/main.cs | 266 +++++ samples/Textured/shaders/main.frag | 15 + samples/Textured/shaders/main.vert | 28 + samples/TexturedCube/TexturedCube.csproj | 7 + samples/TexturedCube/main.cs | 330 ++++++ .../TexturedCube/shaders/FullScreenQuad.vert | 10 + samples/TexturedCube/shaders/main.frag | 15 + samples/TexturedCube/shaders/main.vert | 28 + .../TexturedCube/shaders/simpletexture.frag | 15 + samples/TexturedCube/shaders/skybox.frag | 15 + samples/TexturedCube/shaders/skybox.vert | 26 + samples/Triangle/Triangle.csproj | 6 + samples/Triangle/main.cs | 184 ++++ samples/Triangle/shaders/main.frag | 9 + samples/Triangle/shaders/main.vert | 30 + samples/common/CrowWin.cs | 254 +++++ samples/common/shaders/preamble.inc | 4 + samples/compute/compute.csproj | 10 + samples/compute/delaunay.cs | 329 ++++++ samples/compute/main.cs | 133 +++ samples/compute/shaders/FullScreenQuad.vert | 10 + samples/compute/shaders/compute.comp | 23 + samples/compute/shaders/computeTest.comp | 80 ++ samples/compute/shaders/delaunay.comp | 70 ++ samples/compute/shaders/init.comp | 36 + samples/compute/shaders/mandelbrot.comp | 56 + samples/compute/shaders/normalize.comp | 73 ++ samples/compute/shaders/simpletexture.frag | 15 + samples/compute/shaders/test.comp | 21 + samples/compute/shaders/triangle2.frag | 13 + samples/compute/shaders/triangle2.vert | 24 + samples/compute/test.cs | 133 +++ samples/compute/test2.cs | 389 +++++++ samples/deferred/DebuDrawPipeline.cs | 78 ++ samples/deferred/DeferredPbrRenderer.cs | 528 ++++++++++ samples/deferred/EnvironmentPipeline.cs | 318 ++++++ samples/deferred/deferred.csproj | 43 + samples/deferred/main-crow.cs | 404 +++++++ samples/deferred/main.cs | 549 ++++++++++ samples/deferred/mainShadow.cs | 415 ++++++++ samples/deferred/mainWithDebugDrawer.cs | 412 ++++++++ samples/deferred/modelWithVkvgStats.cs | 359 +++++++ samples/deferred/shaders/FullScreenQuad.vert | 10 + samples/deferred/shaders/GBuffPbr.frag | 212 ++++ samples/deferred/shaders/GBuffPbr.vert | 37 + .../deferred/shaders/GBuffPbrTexArray.frag | 211 ++++ samples/deferred/shaders/bloom.comp | 60 ++ samples/deferred/shaders/bloom.frag | 63 ++ samples/deferred/shaders/compose.frag | 230 ++++ .../shaders/compose_with_shadows.frag | 277 +++++ samples/deferred/shaders/debug.frag | 12 + samples/deferred/shaders/debug.vert | 34 + samples/deferred/shaders/emissive.frag | 25 + samples/deferred/shaders/filtercube.vert | 19 + samples/deferred/shaders/genbrdflut.frag | 90 ++ samples/deferred/shaders/genbrdflut.vert | 9 + samples/deferred/shaders/irradiancecube.frag | 37 + samples/deferred/shaders/prefilterenvmap.frag | 105 ++ samples/deferred/shaders/shadow.geom | 34 + samples/deferred/shaders/shadow.vert | 12 + samples/deferred/shaders/show_gbuff.frag | 92 ++ samples/deferred/shaders/simpletexture.frag | 15 + samples/deferred/shaders/skybox.frag | 12 + samples/deferred/shaders/skybox.vert | 27 + samples/deferred/shaders/tone_mapping.frag | 70 ++ samples/deferred/shadowMapRenderer.cs | 157 +++ samples/deferred/ui/debug.crow | 13 + samples/deferred/ui/deferred.style | 61 ++ samples/deferred/ui/main.crow | 37 + samples/deferred/ui/materials.crow | 30 + samples/deferred/ui/menu.crow | 9 + samples/deferred/ui/sceneItem.crow | 18 + samples/deferred/ui/scenes.crow | 41 + samples/deferred/ui/testImage.crow | 3 + samples/pbr/EnvironmentPipeline.cs | 354 +++++++ samples/pbr/PbrModel.cs | 39 + samples/pbr/PbrPipeline.cs | 158 +++ samples/pbr/main.cs | 430 ++++++++ samples/pbr/pbr.csproj | 24 + samples/pbr/shaders/FullScreenQuad.vert | 10 + samples/pbr/shaders/filtercube.vert | 20 + samples/pbr/shaders/genbrdflut.frag | 90 ++ samples/pbr/shaders/genbrdflut.vert | 9 + samples/pbr/shaders/irradiancecube.frag | 37 + samples/pbr/shaders/pbr.vert | 44 + samples/pbr/shaders/pbr_khr.frag | 459 ++++++++ samples/pbr/shaders/prefilterenvmap.frag | 105 ++ samples/pbr/shaders/simpletexture.frag | 15 + samples/pbr/shaders/skybox.frag | 60 ++ samples/pbr/shaders/skybox.vert | 27 + samples/pbr/ui/fps.crow | 22 + samples/tests/Program.cs | 48 + samples/tests/shaders/main.vert | 28 + samples/tests/tests.csproj | 6 + vke.net.sln | 125 +++ vke/netfx.props | 1 + vke/shaders/FullScreenQuad.vert | 10 + vke/shaders/simpletexture.frag | 15 + vke/src/Camera.cs | 130 +++ vke/src/ExtensionMethods.cs | 144 +++ vke/src/FixedUtf8String.cs | 85 ++ vke/src/MarshaledObject.cs | 65 ++ vke/src/MemoryPool.cs | 147 +++ vke/src/ResourceManager.cs | 56 + vke/src/ShaderInfo.cs | 47 + vke/src/SpecializationConstant.cs | 123 +++ vke/src/StbImage.cs | 65 ++ vke/src/Utils.cs | 289 ++++++ vke/src/Version.cs | 23 + vke/src/VertexAttribute.cs | 42 + vke/src/VkFormatSize.cs | 548 ++++++++++ vke/src/VkWindow.cs | 348 +++++++ vke/src/base/Activable.cs | 134 +++ vke/src/base/Buffer.cs | 111 ++ vke/src/base/CommandBuffer.cs | 188 ++++ vke/src/base/CommandPool.cs | 147 +++ vke/src/base/ComputePipeline.cs | 81 ++ vke/src/base/DebuDrawPipeline.cs | 83 ++ vke/src/base/DebugReport.cs | 114 ++ vke/src/base/DebugUtilsMessenger.cs | 95 ++ vke/src/base/DescriptorPool.cs | 90 ++ vke/src/base/DescriptorSet.cs | 28 + vke/src/base/DescriptorSetLayout.cs | 60 ++ vke/src/base/DescriptorSetWrites.cs | 227 ++++ vke/src/base/Device.cs | 323 ++++++ vke/src/base/FrameBuffer.cs | 120 +++ vke/src/base/GPUBuffer.cs | 68 ++ vke/src/base/GraphicPipeline.cs | 135 +++ vke/src/base/GraphicPipelineConfig.cs | 143 +++ vke/src/base/HostBuffer.cs | 109 ++ vke/src/base/Image.cs | 643 ++++++++++++ vke/src/base/Instance.cs | 192 ++++ vke/src/base/PhysicalDevice.cs | 203 ++++ vke/src/base/Pipeline.cs | 72 ++ vke/src/base/PipelineCache.cs | 142 +++ vke/src/base/PipelineLayout.cs | 113 ++ vke/src/base/QueryPool.cs | 164 +++ vke/src/base/Queue.cs | 136 +++ vke/src/base/RenderPass.cs | 260 +++++ vke/src/base/Resource.cs | 108 ++ vke/src/base/SubPass.cs | 120 +++ vke/src/base/SwapChain.cs | 183 ++++ vke/src/gl.cs | 982 ++++++++++++++++++ vke/src/glfw/CodePoint.cs | 43 + vke/src/glfw/Delegates.cs | 123 +++ vke/src/glfw/ErrorCode.cs | 49 + vke/src/glfw/Glfw3.cs | 534 ++++++++++ vke/src/glfw/InputAction.cs | 21 + vke/src/glfw/Key.cs | 130 +++ vke/src/glfw/Modifier.cs | 16 + vke/src/glfw/MonitorEvent.cs | 17 + vke/src/glfw/MonitorHandle.cs | 34 + vke/src/glfw/MouseButton.cs | 57 + vke/src/glfw/NativeString.cs | 50 + vke/src/glfw/VideoMode.cs | 47 + vke/src/glfw/VideoModePointer.cs | 34 + vke/src/glfw/WindowAttribute.cs | 180 ++++ vke/src/ktx.cs | 209 ++++ vke/src/model/BoundingBox.cs | 78 ++ vke/src/model/Model.cs | 171 +++ vke/vke.csproj | 54 + 196 files changed, 22398 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Directory.Build.props create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 SpirVTasks/CompileGLSLTask.cs create mode 100644 SpirVTasks/README.md create mode 100644 SpirVTasks/SpirVTasks.csproj create mode 100644 SpirVTasks/SpirVTasks.targets create mode 100644 SpirVTasks/spirv.xml create mode 100644 addons/Directory.Build.props create mode 100644 addons/DistanceFieldFont/BMChar.cs create mode 100644 addons/DistanceFieldFont/BMFont.cs create mode 100644 addons/DistanceFieldFont/DistanceFieldFont.csproj create mode 100644 addons/VkvgPipeline/VkvgPipeline.cs create mode 100644 addons/VkvgPipeline/VkvgPipeline.csproj create mode 100644 addons/gltfLoader/PbrModel.cs create mode 100644 addons/gltfLoader/PbrModelSeparateTextures.cs create mode 100644 addons/gltfLoader/PbrModelTexArray.cs create mode 100644 addons/gltfLoader/glTFLoader.cs create mode 100644 addons/gltfLoader/gltfLoader.csproj create mode 100755 clean.sh create mode 100644 netfx.props create mode 100644 samples/Directory.Build.props create mode 100644 samples/DistanceFieldFontTest/DistanceFieldFontTest.csproj create mode 100644 samples/DistanceFieldFontTest/Program.cs create mode 100644 samples/DistanceFieldFontTest/shaders/main.frag create mode 100644 samples/DistanceFieldFontTest/shaders/main.vert create mode 100644 samples/Model/Model.csproj create mode 100644 samples/Model/data/DamagedHelmet.bin create mode 100644 samples/Model/data/DamagedHelmet.gltf create mode 100644 samples/Model/main.cs create mode 100644 samples/Model/shaders/model.frag create mode 100644 samples/Model/shaders/model.vert create mode 100644 samples/Textured/Textured.csproj create mode 100644 samples/Textured/main.cs create mode 100644 samples/Textured/shaders/main.frag create mode 100644 samples/Textured/shaders/main.vert create mode 100644 samples/TexturedCube/TexturedCube.csproj create mode 100644 samples/TexturedCube/main.cs create mode 100644 samples/TexturedCube/shaders/FullScreenQuad.vert create mode 100644 samples/TexturedCube/shaders/main.frag create mode 100644 samples/TexturedCube/shaders/main.vert create mode 100644 samples/TexturedCube/shaders/simpletexture.frag create mode 100644 samples/TexturedCube/shaders/skybox.frag create mode 100644 samples/TexturedCube/shaders/skybox.vert create mode 100644 samples/Triangle/Triangle.csproj create mode 100644 samples/Triangle/main.cs create mode 100644 samples/Triangle/shaders/main.frag create mode 100644 samples/Triangle/shaders/main.vert create mode 100644 samples/common/CrowWin.cs create mode 100644 samples/common/shaders/preamble.inc create mode 100644 samples/compute/compute.csproj create mode 100644 samples/compute/delaunay.cs create mode 100644 samples/compute/main.cs create mode 100644 samples/compute/shaders/FullScreenQuad.vert create mode 100644 samples/compute/shaders/compute.comp create mode 100644 samples/compute/shaders/computeTest.comp create mode 100644 samples/compute/shaders/delaunay.comp create mode 100644 samples/compute/shaders/init.comp create mode 100644 samples/compute/shaders/mandelbrot.comp create mode 100644 samples/compute/shaders/normalize.comp create mode 100644 samples/compute/shaders/simpletexture.frag create mode 100644 samples/compute/shaders/test.comp create mode 100644 samples/compute/shaders/triangle2.frag create mode 100644 samples/compute/shaders/triangle2.vert create mode 100644 samples/compute/test.cs create mode 100644 samples/compute/test2.cs create mode 100644 samples/deferred/DebuDrawPipeline.cs create mode 100644 samples/deferred/DeferredPbrRenderer.cs create mode 100644 samples/deferred/EnvironmentPipeline.cs create mode 100644 samples/deferred/deferred.csproj create mode 100644 samples/deferred/main-crow.cs create mode 100644 samples/deferred/main.cs create mode 100644 samples/deferred/mainShadow.cs create mode 100644 samples/deferred/mainWithDebugDrawer.cs create mode 100644 samples/deferred/modelWithVkvgStats.cs create mode 100644 samples/deferred/shaders/FullScreenQuad.vert create mode 100644 samples/deferred/shaders/GBuffPbr.frag create mode 100644 samples/deferred/shaders/GBuffPbr.vert create mode 100644 samples/deferred/shaders/GBuffPbrTexArray.frag create mode 100644 samples/deferred/shaders/bloom.comp create mode 100644 samples/deferred/shaders/bloom.frag create mode 100644 samples/deferred/shaders/compose.frag create mode 100644 samples/deferred/shaders/compose_with_shadows.frag create mode 100644 samples/deferred/shaders/debug.frag create mode 100644 samples/deferred/shaders/debug.vert create mode 100644 samples/deferred/shaders/emissive.frag create mode 100644 samples/deferred/shaders/filtercube.vert create mode 100644 samples/deferred/shaders/genbrdflut.frag create mode 100644 samples/deferred/shaders/genbrdflut.vert create mode 100644 samples/deferred/shaders/irradiancecube.frag create mode 100644 samples/deferred/shaders/prefilterenvmap.frag create mode 100644 samples/deferred/shaders/shadow.geom create mode 100644 samples/deferred/shaders/shadow.vert create mode 100644 samples/deferred/shaders/show_gbuff.frag create mode 100644 samples/deferred/shaders/simpletexture.frag create mode 100644 samples/deferred/shaders/skybox.frag create mode 100644 samples/deferred/shaders/skybox.vert create mode 100644 samples/deferred/shaders/tone_mapping.frag create mode 100644 samples/deferred/shadowMapRenderer.cs create mode 100644 samples/deferred/ui/debug.crow create mode 100644 samples/deferred/ui/deferred.style create mode 100644 samples/deferred/ui/main.crow create mode 100644 samples/deferred/ui/materials.crow create mode 100644 samples/deferred/ui/menu.crow create mode 100644 samples/deferred/ui/sceneItem.crow create mode 100644 samples/deferred/ui/scenes.crow create mode 100644 samples/deferred/ui/testImage.crow create mode 100644 samples/pbr/EnvironmentPipeline.cs create mode 100644 samples/pbr/PbrModel.cs create mode 100644 samples/pbr/PbrPipeline.cs create mode 100644 samples/pbr/main.cs create mode 100644 samples/pbr/pbr.csproj create mode 100644 samples/pbr/shaders/FullScreenQuad.vert create mode 100644 samples/pbr/shaders/filtercube.vert create mode 100644 samples/pbr/shaders/genbrdflut.frag create mode 100644 samples/pbr/shaders/genbrdflut.vert create mode 100644 samples/pbr/shaders/irradiancecube.frag create mode 100644 samples/pbr/shaders/pbr.vert create mode 100644 samples/pbr/shaders/pbr_khr.frag create mode 100644 samples/pbr/shaders/prefilterenvmap.frag create mode 100644 samples/pbr/shaders/simpletexture.frag create mode 100644 samples/pbr/shaders/skybox.frag create mode 100644 samples/pbr/shaders/skybox.vert create mode 100755 samples/pbr/ui/fps.crow create mode 100644 samples/tests/Program.cs create mode 100644 samples/tests/shaders/main.vert create mode 100644 samples/tests/tests.csproj create mode 100644 vke.net.sln create mode 120000 vke/netfx.props create mode 100644 vke/shaders/FullScreenQuad.vert create mode 100644 vke/shaders/simpletexture.frag create mode 100644 vke/src/Camera.cs create mode 100644 vke/src/ExtensionMethods.cs create mode 100644 vke/src/FixedUtf8String.cs create mode 100644 vke/src/MarshaledObject.cs create mode 100644 vke/src/MemoryPool.cs create mode 100644 vke/src/ResourceManager.cs create mode 100644 vke/src/ShaderInfo.cs create mode 100644 vke/src/SpecializationConstant.cs create mode 100644 vke/src/StbImage.cs create mode 100644 vke/src/Utils.cs create mode 100644 vke/src/Version.cs create mode 100644 vke/src/VertexAttribute.cs create mode 100644 vke/src/VkFormatSize.cs create mode 100644 vke/src/VkWindow.cs create mode 100644 vke/src/base/Activable.cs create mode 100644 vke/src/base/Buffer.cs create mode 100644 vke/src/base/CommandBuffer.cs create mode 100644 vke/src/base/CommandPool.cs create mode 100644 vke/src/base/ComputePipeline.cs create mode 100644 vke/src/base/DebuDrawPipeline.cs create mode 100644 vke/src/base/DebugReport.cs create mode 100644 vke/src/base/DebugUtilsMessenger.cs create mode 100644 vke/src/base/DescriptorPool.cs create mode 100644 vke/src/base/DescriptorSet.cs create mode 100644 vke/src/base/DescriptorSetLayout.cs create mode 100644 vke/src/base/DescriptorSetWrites.cs create mode 100644 vke/src/base/Device.cs create mode 100644 vke/src/base/FrameBuffer.cs create mode 100644 vke/src/base/GPUBuffer.cs create mode 100644 vke/src/base/GraphicPipeline.cs create mode 100644 vke/src/base/GraphicPipelineConfig.cs create mode 100644 vke/src/base/HostBuffer.cs create mode 100644 vke/src/base/Image.cs create mode 100644 vke/src/base/Instance.cs create mode 100644 vke/src/base/PhysicalDevice.cs create mode 100644 vke/src/base/Pipeline.cs create mode 100644 vke/src/base/PipelineCache.cs create mode 100644 vke/src/base/PipelineLayout.cs create mode 100644 vke/src/base/QueryPool.cs create mode 100644 vke/src/base/Queue.cs create mode 100644 vke/src/base/RenderPass.cs create mode 100644 vke/src/base/Resource.cs create mode 100644 vke/src/base/SubPass.cs create mode 100644 vke/src/base/SwapChain.cs create mode 100644 vke/src/gl.cs create mode 100644 vke/src/glfw/CodePoint.cs create mode 100644 vke/src/glfw/Delegates.cs create mode 100644 vke/src/glfw/ErrorCode.cs create mode 100644 vke/src/glfw/Glfw3.cs create mode 100644 vke/src/glfw/InputAction.cs create mode 100644 vke/src/glfw/Key.cs create mode 100644 vke/src/glfw/Modifier.cs create mode 100644 vke/src/glfw/MonitorEvent.cs create mode 100644 vke/src/glfw/MonitorHandle.cs create mode 100644 vke/src/glfw/MouseButton.cs create mode 100644 vke/src/glfw/NativeString.cs create mode 100644 vke/src/glfw/VideoMode.cs create mode 100644 vke/src/glfw/VideoModePointer.cs create mode 100644 vke/src/glfw/WindowAttribute.cs create mode 100644 vke/src/ktx.cs create mode 100644 vke/src/model/BoundingBox.cs create mode 100644 vke/src/model/Model.cs create mode 100644 vke/vke.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1aece0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.vs +build/ +packages/ +bin/ +obj/ +*.user +*.userprefs diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..610673e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: csharp +dist: xenial + +dotnet: 2.1.502 +mono: latest + +script: + - nuget restore + - msbuild + - dotnet restore + - dotnet build + diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..449260d --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,14 @@ + + + $(MSBuildThisFileDirectory) + true + + net471;netstandard2.0 + + https://github.com/jpbruyere/vke.net + MIT + Jean-Philippe Bruyère + + 7.2 + + diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..2c2a0a1 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2016] [Jean-Philippe Bruyère] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e59709 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +

+vke.net +
+

+ + + + +

+

+ +

+ + + +
adaptation of the gltf PBR sample from Sacha Willems
+

+**Vulkan Engine for .net** is composed of high level classes encapsulating vulkan objects with `IDispose` model and **reference counting**. [GLFW](https://www.glfw.org/) handles the windowing system. + +### Requirements +- [GLFW](https://www.glfw.org/) +- [libstb](https://github.com/nothings/stb), on debian install **libstb-dev**. +- [Vulkan Sdk](https://www.lunarg.com/vulkan-sdk/), **glslc** has to be in the path. +- optionaly for ui, you will need [vkvg](https://github.com/jpbruyere/vkvg). + +### Features + +- physicaly based rendering, direct and deferred +- glTF 2.0 +- ktx image loading. +- Memory pools + +### VkWindow class + +To create a new vulkan application, derrive your application from `VkWindow`. Validation and +debug reports may be activated with the static Fields of the `Instance` class. + +```csharp +class Program : VkWindow { + static void Main (string[] args) { + Instance.VALIDATION = true; + + using (Program vke = new Program ()) { + vke.Run (); + } + } +} +``` + +### Enabling features + +Override the `configureEnabledFeatures` method of `VkWindow` to enable features. +```csharp +protected override void configureEnabledFeatures (VkPhysicalDeviceFeatures available_features, ref VkPhysicalDeviceFeatures enabled_features) { + enabled_features.samplerAnisotropy = available_features.samplerAnisotropy; +} +``` +### Creating queues + +To create queues, override the `createQueues` method of `VkWindow`. This function is called before the logical device creation and will take care of physically available queues, creating duplicates if count exceed availability. The `base` method will create a default presentable queue. + +```csharp +protected override void createQueues () { + base.createQueues (); + transferQ = new Queue (dev, VkQueueFlags.Transfer); +} +``` +### Rendering + +The constructor of the `VkWIndow` will finish the vulkan initialisation, so that you may create pipelines, buffers, and so on in your constructor. + +VkWindow will provide the default swapchain, but it's up to you to create the frame buffers. For the triangle example, create them in the `OnResize` override. +```csharp +Framebuffer[] frameBuffers; + +protected override void OnResize () { + if (frameBuffers != null) + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); + frameBuffers = new Framebuffer[swapChain.ImageCount]; + + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i] = new Framebuffer (pipeline.RenderPass, swapChain.Width, swapChain.Height, + (pipeline.Samples == VkSampleCountFlags.SampleCount1) ? new Image[] { + swapChain.images[i], + null + } : new Image[] { + null, + null, + swapChain.images[i] + }); + + buildCommandBuffers (); +} +``` + diff --git a/SpirVTasks/CompileGLSLTask.cs b/SpirVTasks/CompileGLSLTask.cs new file mode 100644 index 0000000..cac29fc --- /dev/null +++ b/SpirVTasks/CompileGLSLTask.cs @@ -0,0 +1,203 @@ +using System; +using System.Diagnostics; +using System.IO; +using Microsoft.Build.Framework; + +namespace SpirVTasks { + + public class IncludeFileNotFound : FileNotFoundException { + public string SourceFile; + public int SourceLine; + + public IncludeFileNotFound(string srcFileName, int srcLine, string includeFileName) : + base ("include file not found", includeFileName) { + + SourceFile = srcFileName; + SourceLine = srcLine; + } + } + + public class CompileGLSLTask : Microsoft.Build.Utilities.Task { + + [Required] + public ITaskItem SourceFile { + get; + set; + } + [Required] + public ITaskItem TempDirectory { + get; + set; + } + public ITaskItem AdditionalIncludeDirectories { + get; + set; + } + public ITaskItem SpirVCompilerPath { + get; + set; + } + [Required] + [Output] + public ITaskItem DestinationFile { + get; + set; + } + + volatile bool success; + + bool tryFindInclude (string include, out string incFile) { + if (!string.IsNullOrEmpty (AdditionalIncludeDirectories?.ItemSpec)) { + foreach (string incDir in AdditionalIncludeDirectories.ItemSpec.Split (';', ',', '|')) { + incFile = Path.Combine (incDir, include); + if (File.Exists (incFile)) + return true; + } + } + incFile = ""; + return false; + } + + void build_source (string src, StreamWriter temp) { + using (StreamReader sr = new StreamReader (File.OpenRead (src))) { + int srcLine = 0; + while (!sr.EndOfStream) { + string line = sr.ReadLine (); + if (line.Trim ().StartsWith ("#include", StringComparison.Ordinal)) { + string include = line.Split ('"', '<', '>')[1]; + string incFile = Path.Combine (Path.GetDirectoryName (src), include); + if (!File.Exists (incFile)) { + if (!tryFindInclude(include, out incFile)) + throw new IncludeFileNotFound (src, srcLine, include); + } + build_source (incFile, temp); + } else + temp.WriteLine (line); + srcLine++; + } + } + } + + bool tryFindGlslcExecutable (out string glslcPath) { + if (!string.IsNullOrEmpty (SpirVCompilerPath?.ItemSpec)) { + glslcPath = SpirVCompilerPath.ItemSpec; + if (!File.Exists (glslcPath)) + return false; + } + + string glslcExec = "glslc"; + if (Environment.OSVersion.Platform.ToString ().StartsWith ("Win", StringComparison.Ordinal)) + glslcExec = glslcExec + "exe"; + + glslcPath = Path.Combine (Environment.GetEnvironmentVariable ("VULKAN_SDK"), "bin"); + glslcPath = Path.Combine (glslcPath, glslcExec); + if (File.Exists (glslcPath)) + return true; + + string envStrPathes = Environment.GetEnvironmentVariable ("PATH"); + if (!string.IsNullOrEmpty (envStrPathes)) { + foreach (string path in envStrPathes.Split (';')) { + glslcPath = Path.Combine (path, glslcExec); + if (File.Exists (glslcPath)) + return true; + } + } + return false; + } + + + public override bool Execute () { + + success = true; + + if (!tryFindGlslcExecutable(out string glslcPath)) { + BuildErrorEventArgs err = new BuildErrorEventArgs ("execute", "VK001", BuildEngine.ProjectFileOfTaskNode, 0, 0, 0, 0, $"glslc command not found: {glslcPath}", "Set 'VULKAN_SDK' environment variable", "SpirVTasks"); + BuildEngine.LogErrorEvent (err); + return false; + } + + string tempFile = Path.Combine (TempDirectory.ItemSpec, SourceFile.ItemSpec); + if (File.Exists (tempFile)) + File.Delete (tempFile); + try { + Directory.CreateDirectory (Path.GetDirectoryName (tempFile)); + using (StreamWriter sw = new StreamWriter (File.OpenWrite(tempFile))) { + string src = SourceFile.ItemSpec; + build_source (SourceFile.ItemSpec, sw); + } + } catch (IncludeFileNotFound ex) { + BuildErrorEventArgs err = new BuildErrorEventArgs ("include", "VK002", ex.SourceFile, ex.SourceLine, 0, 0, 0, $"include file not found: {ex.FileName}", "", "SpirVTasks"); + BuildEngine.LogErrorEvent (err); + return false; + }catch (Exception ex) { + BuildErrorEventArgs err = new BuildErrorEventArgs ("include", "VK000", SourceFile.ItemSpec, 0, 0, 0, 0, ex.ToString(), "", "SpirVTasks"); + BuildEngine.LogErrorEvent (err); + return false; + } + + Directory.CreateDirectory (Path.GetDirectoryName (DestinationFile.ItemSpec)); + + Process glslc = new Process(); + //glslc.StartInfo.StandardOutputEncoding = System.Text.Encoding.ASCII; + //glslc.StartInfo.StandardErrorEncoding = System.Text.Encoding.ASCII; + glslc.StartInfo.UseShellExecute = false; + glslc.StartInfo.RedirectStandardOutput = true; + glslc.StartInfo.RedirectStandardError = true; + glslc.StartInfo.FileName = glslcPath; + glslc.StartInfo.Arguments = $"{tempFile} -o {DestinationFile.ItemSpec}"; + glslc.StartInfo.CreateNoWindow = true; + + glslc.EnableRaisingEvents = true; + glslc.OutputDataReceived += Glslc_OutputDataReceived; + glslc.ErrorDataReceived += Glslc_ErrorDataReceived; + + glslc.Start (); + + glslc.BeginErrorReadLine (); + glslc.BeginOutputReadLine (); + + DestinationFile.SetMetadata ("LogicalName", $"FromCS.{SourceFile.ItemSpec.Replace (Path.DirectorySeparatorChar, '.')}"); + + glslc.WaitForExit (); + + return success; + } + + void Glslc_ErrorDataReceived (object sender, DataReceivedEventArgs e) { + if (e.Data == null) + return; + + if (string.Equals (e.Data, "(0)", StringComparison.Ordinal)) + return; + + string[] tmp = e.Data.Split (':'); + + Log.LogMessage (MessageImportance.High, $"glslc: {e.Data}"); + + if (tmp.Length == 5) { + string srcFile = SourceFile.ItemSpec; + int line = Math.Max (0, int.Parse (tmp[1]) - 1); + + BuildErrorEventArgs err = new BuildErrorEventArgs ("compile", tmp[2], srcFile, line, 0, 0, 0, $"{tmp[3]} {tmp[4]}", "no help", "SpirVTasks"); + BuildEngine.LogErrorEvent (err); + success = false; + } else { + Log.LogMessage (MessageImportance.High, $"{e.Data}"); + } + } + + + void Glslc_OutputDataReceived (object sender, DataReceivedEventArgs e) { + if (e.Data == null) + return; + if (string.Equals (e.Data, "(0)", StringComparison.Ordinal)) + return; + + Log.LogMessage (MessageImportance.High, $"data:{e.Data}"); + + BuildWarningEventArgs taskEvent = new BuildWarningEventArgs ("glslc", "0", BuildEngine.ProjectFileOfTaskNode, 0, 0, 0, 0, $"{e.Data}", "no help", "SpirVTasks"); + BuildEngine.LogWarningEvent (taskEvent); + } + + } +} diff --git a/SpirVTasks/README.md b/SpirVTasks/README.md new file mode 100644 index 0000000..226e438 --- /dev/null +++ b/SpirVTasks/README.md @@ -0,0 +1,62 @@ +

+ SpirVTasks MSBuild add-on +
+

+ + + + + + +

+

+ +**SpirVTasks** package add **SpirV** compilation support to msbuild project. Error and warning +are routed to the **IDE**. + + +#### Usage +```xml + + + +``` +Resulting `.spv` files are embedded with resource ID = **ProjectName.file.ext.spv**. You can override the default resource id by adding a custom LogicalName. +```xml + + + NewName.vert.spv + + +``` +**VULKAN_SDK**/bin then **PATH** are searched for the **glslc** executable. You can also use **`SpirVglslcPath`** property. +```xml + + bin\glslc.exe + +``` + + +#### Include in glsl +```glsl +#include + +layout (location = 0) in vec3 inColor; +layout (location = 0) out vec4 outFragColor; + +void main() +{ + outFragColor = vec4(inColor, 1.0); +} +``` + +Included files are searched from the location of the current parsed file, then in the **``** directories if present. + +```xml + + $(MSBuildThisFileDirectory)common;testdir;../anotherdir + +``` + +#### todo +- Error source file and line with included files. diff --git a/SpirVTasks/SpirVTasks.csproj b/SpirVTasks/SpirVTasks.csproj new file mode 100644 index 0000000..314ddc7 --- /dev/null +++ b/SpirVTasks/SpirVTasks.csproj @@ -0,0 +1,39 @@ + + + net471;netstandard2.0 + 0.1.9 + MSBuild addon to compile and embed spirV shaders + + SpirVTasks + vulkan msbuild spirv glsl addons + $(AssemblyVersion)-beta + True + False + https://github.com/jpbruyere/vk.net/blob/master/SpirVTasks/README.md + MIT + Jean-Philippe Bruyère + $(SolutionDir)build\$(Configuration)\ + + false + false + false + true + + SpirVTasks_$(TargetFramework) + + + + + + + true + true + build\ + + + true + true + build + + + diff --git a/SpirVTasks/SpirVTasks.targets b/SpirVTasks/SpirVTasks.targets new file mode 100644 index 0000000..6825c47 --- /dev/null +++ b/SpirVTasks/SpirVTasks.targets @@ -0,0 +1,30 @@ + + + + + + DXC + + + + + + + + + + + + + diff --git a/SpirVTasks/spirv.xml b/SpirVTasks/spirv.xml new file mode 100644 index 0000000..9635ad6 --- /dev/null +++ b/SpirVTasks/spirv.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/addons/Directory.Build.props b/addons/Directory.Build.props new file mode 100644 index 0000000..0e3fb55 --- /dev/null +++ b/addons/Directory.Build.props @@ -0,0 +1,45 @@ + + + $(MSBuildThisFileDirectory)../ + true + + net471;netstandard2.0 + + https://github.com/jpbruyere/vke.net + MIT + Jean-Philippe Bruyère + + $(SolutionDir)build\$(Configuration)\ + true + false + + 7.2 + + $(MSBuildThisFileDirectory)common\shaders + + + + + TRACE;DEBUG;NETSTANDARD;NETSTANDARD2_0;_WITH_SHADOWS;WITH_VKVG + + + NETSTANDARD;NETSTANDARD2_0;WITH_SHADOWS;_WITH_VKVG + + + + + + + + + + + + + + + + + + + diff --git a/addons/DistanceFieldFont/BMChar.cs b/addons/DistanceFieldFont/BMChar.cs new file mode 100644 index 0000000..0667fc3 --- /dev/null +++ b/addons/DistanceFieldFont/BMChar.cs @@ -0,0 +1,49 @@ +// Copyright (c) 2019 Jean-Philippe Bruyère +// +// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) +using System; +namespace CVKL.DistanceFieldFont { + public struct BMChar { + public enum Channel { + Blue = 0x01, + Green = 0x02, + Red = 0x04, + Alpha = 0x08, + All = 0x0f + } + public uint id; + /// + /// The left position of the character image in the texture. + /// + public uint x; + public uint y; + /// + /// The width of the character image in the texture. + /// + public uint width; + /// + /// The height of the character image in the texture. + /// + public uint height; + /// + /// How much the current position should be offset when copying the image from the texture to the screen. + /// + public int xoffset; + /// + /// How much the current position should be offset when copying the image from the texture to the screen. + /// + public int yoffset; + /// + /// How much the current position should be advanced after drawing the character. + /// + public int xadvance; + /// + /// The texture page where the character image is found. + /// + public uint page; + /// + /// The texture channel where the character image is found (1 = blue, 2 = green, 4 = red, 8 = alpha, 15 = all channels). + /// + public Channel channel; + } +} diff --git a/addons/DistanceFieldFont/BMFont.cs b/addons/DistanceFieldFont/BMFont.cs new file mode 100644 index 0000000..e277517 --- /dev/null +++ b/addons/DistanceFieldFont/BMFont.cs @@ -0,0 +1,207 @@ +// Copyright (c) 2019 Jean-Philippe Bruyère +// +// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using VK; + +namespace CVKL.DistanceFieldFont { + /// + /// BMF font. (http://www.angelcode.com/products/bmfont/doc/file_format.html) + /// + public class BMFont { + /// + /// distance in pixels between each line of text. + /// + public readonly ushort lineHeight; + /// + /// The number of pixels from the absolute top of the line to the base of the characters. + /// + public readonly ushort @base; + public readonly ushort width, height; + + public readonly Dictionary CharMap = new Dictionary (); + public readonly Dictionary PageImagePathes = new Dictionary (); + + readonly string fntFilePath; + + public BMFont (string path) { + fntFilePath = path; + object thisFont = this; + + + using (BMFontStreamReader bmf = new BMFontStreamReader (path)) { + while (!bmf.EndOfStream) { + + string w = bmf.ReadWord (); + if (string.IsNullOrEmpty (w)) + continue; + + if (string.Equals (w, "info", StringComparison.OrdinalIgnoreCase) || + string.Equals (w, "common", StringComparison.OrdinalIgnoreCase)) + bmf.ReadDatas (ref thisFont); + else if (string.Equals (w, "page", StringComparison.OrdinalIgnoreCase)) { + int p = bmf.ReadPageDatas (out string pageImg); + PageImagePathes.Add (p, pageImg); + } else if (string.Equals (w, "chars", StringComparison.OrdinalIgnoreCase)) + bmf.ReadLine ();//skip char count + else if (string.Equals (w, "char", StringComparison.OrdinalIgnoreCase)) { + BMChar c = bmf.NextBMChar; + CharMap.Add ((int)c.id, c); + } else if (string.Equals (w, "kernings", StringComparison.OrdinalIgnoreCase)) + bmf.ReadLine ();//skip + else if (string.Equals (w, "kerning", StringComparison.OrdinalIgnoreCase)) + bmf.ReadLine ();//skip + } + } + } + + public Image GetPageTexture (int page, Queue staggingQ, CommandPool cmdPool, + VkMemoryPropertyFlags imgProp = VkMemoryPropertyFlags.DeviceLocal, bool genMipMaps = true, VkImageTiling tiling = VkImageTiling.Optimal) { + + string path = Path.Combine (Path.GetDirectoryName (fntFilePath), PageImagePathes[page]); + + if (path.EndsWith ("ktx", StringComparison.OrdinalIgnoreCase)) + return KTX.KTX.Load (staggingQ, cmdPool, path, + VkImageUsageFlags.Sampled, imgProp, genMipMaps, tiling); + return Image.Load (staggingQ.Dev, staggingQ, cmdPool, path, VkFormat.R8g8b8a8Unorm, imgProp, tiling, genMipMaps); + } + + } + + + + internal class BMFontStreamReader : StreamReader { + public BMFontStreamReader (string bmPath) : base (bmPath) { } + + public char PeekedChar => (char)Peek (); + public char ReadedChar => (char)Read (); + public bool EndOfLine => PeekedChar == '\n'; + + public string ReadWord () { + string word = ""; + SkipWhiteSpacesAndLineBreak (); + while (!EndOfStream) { + if (!Char.IsLetterOrDigit (PeekedChar)) + break; + word += ReadedChar; + } + return word; + } + public void SkipWhiteSpaces () { + while (!EndOfStream) { + if (!char.IsWhiteSpace (PeekedChar) || PeekedChar == '\n') + break; + Read (); + } + } + public void SkipWhiteSpacesAndLineBreak () { + while (!EndOfStream) { + if (!char.IsWhiteSpace (PeekedChar)) + break; + Read (); + } + } + + public string NextValue { + get { + SkipWhiteSpaces (); + if (ReadedChar != '=') + throw new Exception ("expecting '='"); + SkipWhiteSpaces (); + if (PeekedChar == '"') { + Read (); + return ReadUntil ('"'); + } + return ReadUntilSpace (); + } + } + public string ReadUntil (char limit) { + string word = ""; + while (!EndOfStream) { + char c = ReadedChar; + if (c == limit) + break; + word += c; + } + return word; + } + public string ReadUntilSpace () { + string word = ""; + while (!EndOfStream) { + if (char.IsWhiteSpace (PeekedChar)) + break; + word += ReadedChar; + } + return word; + } + + public void ReadField (ref object obj, string fieldName) { + FieldInfo fi = obj.GetType ().GetField (fieldName); + if (fi == null) { + System.Diagnostics.Debug.WriteLine ($"BMFont property not handled: {fieldName}"); + string tmp = NextValue; + return; + } + if (fi.FieldType == typeof (ushort)) { + fi.SetValue (obj, ushort.Parse (NextValue)); + return; + } + if (fi.FieldType == typeof (short)) { + fi.SetValue (obj, short.Parse (NextValue)); + return; + } + if (fi.FieldType == typeof (int)) { + fi.SetValue (obj, int.Parse (NextValue)); + return; + } + if (fi.FieldType == typeof (uint)) { + fi.SetValue (obj, uint.Parse (NextValue)); + return; + } + if (fi.FieldType == typeof (string)) { + fi.SetValue (obj, NextValue); + return; + } + } + public void ReadDatas (ref object obj) { + while (!EndOfLine && !EndOfStream) { + string field = ReadWord (); + ReadField (ref obj, field); + } + + } + public int ReadPageDatas (out string pageImgPath) { + int idx = 0; + pageImgPath = ""; + + while (!EndOfLine && !EndOfStream) { + string field = ReadWord (); + if (string.Equals (field, "id", StringComparison.OrdinalIgnoreCase)) + idx = int.Parse (NextValue); + else if (string.Equals (field, "file", StringComparison.OrdinalIgnoreCase)) + pageImgPath = NextValue; + else + Console.WriteLine ($"BMFont page property not handled: {field} = {NextValue}"); + } + return idx; + } + public BMChar NextBMChar { + get { + object c = default (BMChar); + + while (!EndOfLine && !EndOfStream) { + string field = ReadWord (); + ReadField (ref c, field); + SkipWhiteSpaces ();//trailing spaces + } + + return (BMChar)c; + } + + } + } + +} diff --git a/addons/DistanceFieldFont/DistanceFieldFont.csproj b/addons/DistanceFieldFont/DistanceFieldFont.csproj new file mode 100644 index 0000000..a131f27 --- /dev/null +++ b/addons/DistanceFieldFont/DistanceFieldFont.csproj @@ -0,0 +1,20 @@ + + + + + CVKL.DistanceFieldFont + CVKL.DistanceFieldFont + 0.1.0 + CVKL signed distance field font addons, BMFont file format + C# vulkan CVKL gltf + $(AssemblyVersion)-beta + True + False + https://github.com/jpbruyere/vk.net/blob/master/README.md + MIT + + false + true + + + diff --git a/addons/VkvgPipeline/VkvgPipeline.cs b/addons/VkvgPipeline/VkvgPipeline.cs new file mode 100644 index 0000000..1d4ce02 --- /dev/null +++ b/addons/VkvgPipeline/VkvgPipeline.cs @@ -0,0 +1,135 @@ +// Copyright (c) 2019 Jean-Philippe Bruyère +// +// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) +using System; +using System.Numerics; +using CVKL; +using VK; + +namespace VkvgPipeline { + + public class VkvgPipeline : GraphicPipeline { + vkvg.Device vkvgDev; + vkvg.Surface vkvgSurf; + Image uiImage; + + public vkvg.Context CreateContext () => new vkvg.Context (vkvgSurf); + + public VkvgPipeline (Instance instance, Device dev, Queue queue, GraphicPipeline pipeline, PipelineCache pipelineCache = null) + : base (pipeline.RenderPass, pipelineCache, "vkvg pipeline") { + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, this.RenderPass.Samples, false); + cfg.RenderPass = RenderPass; + cfg.Layout = pipeline.Layout; + cfg.AddShader (VkShaderStageFlags.Vertex, "#vke.FullScreenQuad.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "#vke.simpletexture.frag.spv"); + cfg.multisampleState.rasterizationSamples = Samples; + + cfg.blendAttachments[0] = new VkPipelineColorBlendAttachmentState (true); + + layout = cfg.Layout; + + init (cfg); + + vkvgDev = new vkvg.Device (instance.Handle, dev.phy.Handle, dev.VkDev.Handle, queue.qFamIndex, + vkvg.SampleCount.Sample_4, queue.index); + } + + void initUISurface (int width, int height) { + uiImage?.Dispose (); + vkvgSurf?.Dispose (); + vkvgSurf = new vkvg.Surface (vkvgDev, width, height); + uiImage = new Image (Dev, new VkImage ((ulong)vkvgSurf.VkImage.ToInt64 ()), VkFormat.B8g8r8a8Unorm, + VkImageUsageFlags.ColorAttachment, (uint)vkvgSurf.Width, (uint)vkvgSurf.Height); + uiImage.CreateView (VkImageViewType.ImageView2D, VkImageAspectFlags.Color); + uiImage.CreateSampler (VkFilter.Nearest, VkFilter.Nearest, VkSamplerMipmapMode.Nearest, VkSamplerAddressMode.ClampToBorder); + uiImage.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + } + + + public void Resize (int width, int height, DescriptorSetWrites dsUpdate) { + initUISurface (width, height); + dsUpdate.Write (Dev, uiImage.Descriptor); + } + public void RecordDraw (CommandBuffer cmd) { + Bind (cmd); + + uiImage.SetLayout (cmd, VkImageAspectFlags.Color, VkImageLayout.ColorAttachmentOptimal, VkImageLayout.ShaderReadOnlyOptimal, + VkPipelineStageFlags.ColorAttachmentOutput, VkPipelineStageFlags.FragmentShader); + + cmd.Draw (3, 1, 0, 0); + + uiImage.SetLayout (cmd, VkImageAspectFlags.Color, VkImageLayout.ShaderReadOnlyOptimal, VkImageLayout.ColorAttachmentOptimal, + VkPipelineStageFlags.FragmentShader, VkPipelineStageFlags.BottomOfPipe); + } + public void DrawResources (vkvg.Context ctx, int width, int height) { + ResourceManager rm = Dev.resourceManager; + + int margin = 5, memPoolHeight = 15; + int drawingWidth = width - 4 * margin; + int drawingHeight = (memPoolHeight + margin) * (rm.memoryPools.Length) + margin; + int y = height - drawingHeight - margin; + int x = 2 * margin; + ctx.LineWidth = 1; + ctx.SetSource (0.1, 0.1, 0.1, 0.8); + ctx.Rectangle (0.5 + margin, 0.5 + y, width - 2 * margin, drawingHeight); + ctx.FillPreserve (); + ctx.Flush (); + ctx.SetSource (0.8, 0.8, 0.8); + ctx.Stroke (); + + foreach (MemoryPool mp in rm.memoryPools) { + float byteWidth = (float)drawingWidth / mp.Size; + + y += margin; + ctx.Rectangle (x, y, drawingWidth, memPoolHeight); + ctx.SetSource (0.3, 0.3, 0.3, 0.4); + ctx.Fill (); + + if (mp.Last == null) + return; + + Resource r = mp.Last; + do { + float w = Math.Max (1f, byteWidth * r.AllocatedDeviceMemorySize); + + Vector3 c = new Vector3 (0); + Image img = r as Image; + if (img != null) { + c.Z = 1f; + if (img.CreateInfo.usage.HasFlag (VkImageUsageFlags.InputAttachment)) + c.Y += 0.3f; + if (img.CreateInfo.usage.HasFlag (VkImageUsageFlags.ColorAttachment)) + c.Y += 0.1f; + if (img.CreateInfo.usage.HasFlag (VkImageUsageFlags.Sampled)) + c.X += 0.3f; + } else { + CVKL.Buffer buff = r as CVKL.Buffer; + c.X = 1f; + if (buff.Infos.usage.HasFlag (VkBufferUsageFlags.IndexBuffer)) + c.Y += 0.2f; + if (buff.Infos.usage.HasFlag (VkBufferUsageFlags.VertexBuffer)) + c.Y += 0.4f; + if (buff.Infos.usage.HasFlag (VkBufferUsageFlags.UniformBuffer)) + c.Z += 0.3f; + } + ctx.SetSource (c.X, c.Y, c.Z, 0.5); + ctx.Rectangle (0.5f + x + byteWidth * r.poolOffset, 0.5f + y, w, memPoolHeight); + ctx.FillPreserve (); + ctx.SetSource (0.01f, 0.01f, 0.01f); + ctx.Stroke (); + r = r.next; + } while (r != mp.Last && r != null); + y += memPoolHeight; + } + } + protected override void Dispose (bool disposing) { + uiImage?.Dispose (); + vkvgSurf?.Dispose (); + vkvgDev.Dispose (); + + base.Dispose (disposing); + } + } + +} diff --git a/addons/VkvgPipeline/VkvgPipeline.csproj b/addons/VkvgPipeline/VkvgPipeline.csproj new file mode 100644 index 0000000..3d07598 --- /dev/null +++ b/addons/VkvgPipeline/VkvgPipeline.csproj @@ -0,0 +1,23 @@ + + + + + CVKL.vkvgPipeline + CVKL.vkvgPipeline + 0.1.0 + CVKL vkvg addons, vectorial drawing with cairo like api. + C# vulkan CVKL gltf + $(AssemblyVersion)-beta + True + False + https://github.com/jpbruyere/vk.net/blob/master/README.md + MIT + + false + true + + + + + + diff --git a/addons/gltfLoader/PbrModel.cs b/addons/gltfLoader/PbrModel.cs new file mode 100644 index 0000000..396c42f --- /dev/null +++ b/addons/gltfLoader/PbrModel.cs @@ -0,0 +1,124 @@ +// Copyright (c) 2019 Jean-Philippe Bruyère +// +// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) +using System.Diagnostics; +using System.Numerics; +using System.Runtime.InteropServices; + +using VK; +using System.Collections.Generic; + +namespace CVKL.glTF { + //TODO:stride in buffer views? + public abstract class PbrModel : Model { + //public new struct Vertex { + // [VertexAttribute (VertexAttributeType.Position, VkFormat.R32g32b32Sfloat)] + // public Vector3 pos; + // [VertexAttribute (VertexAttributeType.Normal, VkFormat.R32g32b32Sfloat)] + // public Vector3 normal; + // [VertexAttribute (VertexAttributeType.UVs, VkFormat.R32g32Sfloat)] + // public Vector2 uv0; + // [VertexAttribute (VertexAttributeType.UVs, VkFormat.R32g32Sfloat)] + // public Vector2 uv1; + // public override string ToString () { + // return pos.ToString () + ";" + normal.ToString () + ";" + uv0.ToString () + ";" + uv1.ToString (); + // } + //}; + + protected DescriptorPool descriptorPool; + public GPUBuffer vbo; + public GPUBuffer ibo; + public Buffer materialUBO; + + protected PbrModel () { } + public PbrModel (Queue transferQ, string path) { + dev = transferQ.Dev; + using (CommandPool cmdPool = new CommandPool (dev, transferQ.index)) { + using (glTFLoader ctx = new glTFLoader (path, transferQ, cmdPool)) { + loadSolids (ctx); + } + } + } + + protected void loadSolids (glTFLoader ctx) { + loadSolids (ctx); + } + + protected void loadSolids (glTFLoader ctx) { + ulong vertexCount, indexCount; + + ctx.GetVertexCount (out vertexCount, out indexCount, out IndexBufferType); + + ulong vertSize = vertexCount * (ulong)Marshal.SizeOf (); + ulong idxSize = indexCount * (IndexBufferType == VkIndexType.Uint16 ? 2ul : 4ul); + ulong size = vertSize + idxSize; + + vbo = new GPUBuffer (dev, VkBufferUsageFlags.VertexBuffer | VkBufferUsageFlags.TransferDst, vertSize); + ibo = new GPUBuffer (dev, VkBufferUsageFlags.IndexBuffer | VkBufferUsageFlags.TransferDst, idxSize); + + vbo.SetName ("vbo gltf"); + ibo.SetName ("ibo gltf"); + + Meshes = new List (ctx.LoadMeshes (IndexBufferType, vbo, 0, ibo, 0)); + Scenes = new List (ctx.LoadScenes (out defaultSceneIndex)); + } + + /// bind vertex and index buffers + public virtual void Bind (CommandBuffer cmd) { + cmd.BindVertexBuffer (vbo); + cmd.BindIndexBuffer (ibo, IndexBufferType); + } + + //TODO:destset for binding must be variable + //TODO: ADD REFAULT MAT IF NO MAT DEFINED + public abstract void RenderNode (CommandBuffer cmd, PipelineLayout pipelineLayout, Node node, Matrix4x4 currentTransform, bool shadowPass = false); + + public void DrawAll (CommandBuffer cmd, PipelineLayout pipelineLayout, bool shadowPass = false) { + foreach (Scene sc in Scenes) { + foreach (Node node in sc.Root.Children) + RenderNode (cmd, pipelineLayout, node, sc.Root.localMatrix, shadowPass); + } + } + public void Draw(CommandBuffer cmd, PipelineLayout pipelineLayout, Scene scene, bool shadowPass = false) + { + if (scene.Root == null) + return; + RenderNode(cmd, pipelineLayout, scene.Root, Matrix4x4.Identity, shadowPass); + } + + public void Draw (CommandBuffer cmd, PipelineLayout pipelineLayout, Buffer instanceBuf, bool shadowPass = false, params InstancedCmd[] instances) { + cmd.BindVertexBuffer(instanceBuf, 1); + uint firstInstance = 0; + for (int i = 0; i < instances.Length; i++) + { + foreach (Primitive p in Meshes[instances[i].meshIdx].Primitives) + { + if (!shadowPass) + cmd.PushConstant(pipelineLayout, VkShaderStageFlags.Fragment, (int)p.material, (uint)Marshal.SizeOf()); + cmd.DrawIndexed(p.indexCount, instances[i].count, p.indexBase, p.vertexBase, firstInstance); + } + firstInstance += instances[i].count; + } + } + + #region IDisposable Support + protected bool isDisposed; + + protected virtual void Dispose (bool disposing) { + if (!isDisposed) { + if (disposing) { + ibo?.Dispose (); + vbo?.Dispose (); + materialUBO?.Dispose (); + descriptorPool?.Dispose (); + } else + Debug.WriteLine ("model was not disposed"); + isDisposed = true; + } + } + public void Dispose () { + Dispose (true); + } + #endregion + } +} diff --git a/addons/gltfLoader/PbrModelSeparateTextures.cs b/addons/gltfLoader/PbrModelSeparateTextures.cs new file mode 100644 index 0000000..c1a6ee9 --- /dev/null +++ b/addons/gltfLoader/PbrModelSeparateTextures.cs @@ -0,0 +1,162 @@ +// Copyright (c) 2019 Jean-Philippe Bruyère +// +// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) +using System; +using System.Diagnostics; +using System.Numerics; +using System.Runtime.InteropServices; +using VK; + +namespace CVKL.glTF { + /// + /// Indexed pbr model whith one descriptorSet per material with separate textures attachments + /// + public class PbrModelSeparatedTextures : PbrModel { + /// + /// Pbr data structure suitable for push constant or ubo, containing + /// availablility of attached textures and the coef of pbr inputs + /// + public struct Material { + public Vector4 baseColorFactor; + public Vector4 emissiveFactor; + public Vector4 diffuseFactor; + public Vector4 specularFactor; + public float workflow; + public AttachmentType TexCoord0; + public AttachmentType TexCoord1; + public float metallicFactor; + public float roughnessFactor; + public float alphaMask; + public float alphaMaskCutoff; +#pragma warning disable 169 + readonly int pad0;//see std420 +#pragma warning restore 169 + } + + protected Image[] textures; + public Material[] materials; + /// + /// one descriptor per material containing textures + /// + protected DescriptorSet[] descriptorSets; + + protected PbrModelSeparatedTextures () { } + public PbrModelSeparatedTextures (Queue transferQ, string path, DescriptorSetLayout layout, params AttachmentType[] attachments) { + dev = transferQ.Dev; + using (CommandPool cmdPool = new CommandPool (dev, transferQ.index)) { + using (glTFLoader ctx = new glTFLoader (path, transferQ, cmdPool)) { + loadSolids (ctx); + + textures = ctx.LoadImages (); + loadMaterials (ctx, layout, attachments); + + materialUBO = new HostBuffer (dev, VkBufferUsageFlags.UniformBuffer, materials); + + } + } + } + + void loadMaterials (glTFLoader ctx, DescriptorSetLayout layout, params AttachmentType[] attachments) { + glTFLoader.Material[] mats = ctx.LoadMaterial (); + materials = new Material[mats.Length]; + descriptorSets = new DescriptorSet[mats.Length]; + + if (attachments.Length == 0) + throw new InvalidOperationException ("At least one attachment is required for Model.WriteMaterialDescriptor"); + + descriptorPool = new DescriptorPool (dev, (uint)materials.Length, + new VkDescriptorPoolSize (VkDescriptorType.CombinedImageSampler, (uint)(attachments.Length * materials.Length)) + ); + descriptorPool.SetName ("descPool gltfTextures"); + + for (int i = 0; i < mats.Length; i++) { + materials[i] = new Material { + workflow = (float)mats[i].workflow, + baseColorFactor = mats[i].baseColorFactor, + emissiveFactor = mats[i].emissiveFactor, + metallicFactor = mats[i].metallicFactor, + roughnessFactor = mats[i].roughnessFactor, + TexCoord0 = mats[i].availableAttachments, + TexCoord1 = mats[i].availableAttachments1, + alphaMask = 0f, + alphaMaskCutoff = 0.0f, + diffuseFactor = new Vector4 (0), + specularFactor = new Vector4 (0) + }; + + descriptorSets[i] = descriptorPool.Allocate (layout); + descriptorSets[i].Handle.SetDebugMarkerName (dev, "descSet " + mats[i].Name); + + VkDescriptorSetLayoutBinding dslb = + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler); + + using (DescriptorSetWrites2 uboUpdate = new DescriptorSetWrites2 (dev)) { + for (uint a = 0; a < attachments.Length; a++) { + dslb.binding = a; + switch (attachments[a]) { + case AttachmentType.None: + break; + case AttachmentType.Color: + if (mats[i].availableAttachments.HasFlag (AttachmentType.Color)) + uboUpdate.AddWriteInfo (descriptorSets[i], dslb, textures[(int)mats[i].baseColorTexture].Descriptor); + break; + case AttachmentType.Normal: + if (mats[i].availableAttachments.HasFlag (AttachmentType.Normal)) + uboUpdate.AddWriteInfo (descriptorSets[i], dslb, textures[(int)mats[i].normalTexture].Descriptor); + break; + case AttachmentType.AmbientOcclusion: + if (mats[i].availableAttachments.HasFlag (AttachmentType.AmbientOcclusion)) + uboUpdate.AddWriteInfo (descriptorSets[i], dslb, textures[(int)mats[i].occlusionTexture].Descriptor); + break; + case AttachmentType.PhysicalProps: + if (mats[i].availableAttachments.HasFlag (AttachmentType.PhysicalProps)) + uboUpdate.AddWriteInfo (descriptorSets[i], dslb, textures[(int)mats[i].metallicRoughnessTexture].Descriptor); + break; + case AttachmentType.Metal: + break; + case AttachmentType.Roughness: + break; + case AttachmentType.Emissive: + if (mats[i].availableAttachments.HasFlag (AttachmentType.Emissive)) + uboUpdate.AddWriteInfo (descriptorSets[i], dslb, textures[(int)mats[i].emissiveTexture].Descriptor); + break; + } + } + uboUpdate.Update (); + } + } + } + + public override void RenderNode (CommandBuffer cmd, PipelineLayout pipelineLayout, Node node, Matrix4x4 currentTransform, bool shadowPass = false) { + Matrix4x4 localMat = node.localMatrix * currentTransform; + VkShaderStageFlags matStage = shadowPass ? VkShaderStageFlags.Geometry : VkShaderStageFlags.Vertex; + cmd.PushConstant (pipelineLayout, matStage, localMat); + + if (node.Mesh != null) { + foreach (Primitive p in node.Mesh.Primitives) { + if (!shadowPass) { + cmd.PushConstant (pipelineLayout, VkShaderStageFlags.Fragment, (int)p.material, (uint)Marshal.SizeOf ()); + if (descriptorSets[p.material] != null) + cmd.BindDescriptorSet (pipelineLayout, descriptorSets[p.material], 2); + } + cmd.DrawIndexed (p.indexCount, 1, p.indexBase, p.vertexBase, 0); + } + } + if (node.Children == null) + return; + foreach (Node child in node.Children) + RenderNode (cmd, pipelineLayout, child, localMat, shadowPass); + } + + protected override void Dispose (bool disposing) { + if (!isDisposed) { + if (disposing) { + foreach (Image txt in textures) + txt.Dispose (); + } else + Debug.WriteLine ("model was not disposed"); + } + base.Dispose (disposing); + } + } +} diff --git a/addons/gltfLoader/PbrModelTexArray.cs b/addons/gltfLoader/PbrModelTexArray.cs new file mode 100644 index 0000000..5943774 --- /dev/null +++ b/addons/gltfLoader/PbrModelTexArray.cs @@ -0,0 +1,139 @@ +// Copyright (c) 2019 Jean-Philippe Bruyère +// +// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) +using System.Diagnostics; +using System.Numerics; +using System.Runtime.InteropServices; +using VK; + +namespace CVKL.glTF { + /// + /// Indexed pbr model whith one descriptorSet per material with separate textures attachments + /// + public class PbrModelTexArray : PbrModel { + public static uint TEXTURE_DIM = 512; + public new struct Vertex { + [VertexAttribute (VertexAttributeType.Position, VkFormat.R32g32b32Sfloat)] + public Vector3 pos; + [VertexAttribute (VertexAttributeType.Normal, VkFormat.R32g32b32Sfloat)] + public Vector3 normal; + [VertexAttribute (VertexAttributeType.UVs, VkFormat.R32g32Sfloat)] + public Vector2 uv0; + [VertexAttribute (VertexAttributeType.UVs, VkFormat.R32g32Sfloat)] + public Vector2 uv1; + public override string ToString () { + return pos.ToString () + ";" + normal.ToString () + ";" + uv0.ToString () + ";" + uv1.ToString (); + } + }; + + /// + /// Material structure for ubo containing texture indices in tex array + /// + public struct Material { + public Vector4 baseColorFactor; + public Vector4 emissiveFactor; + public Vector4 diffuseFactor; + public Vector4 specularFactor; + + public float workflow; + public AttachmentType TexCoord0; + public AttachmentType TexCoord1; + public int baseColorTextureSet; + + public int phyDescTex; + public int normalTex; + public int aoTex; + public int emissiveTex; + + public float metallicFactor; + public float roughnessFactor; + public float alphaMask; + public float alphaMaskCutoff; + } + + public Image texArray; + public Material[] materials; + + public PbrModelTexArray (Queue transferQ, string path) { + dev = transferQ.Dev; + using (CommandPool cmdPool = new CommandPool (dev, transferQ.index)) { + using (glTFLoader ctx = new glTFLoader (path, transferQ, cmdPool)) { + loadSolids (ctx); + + if (ctx.ImageCount > 0) { + texArray = new Image (dev, Image.DefaultTextureFormat, VkImageUsageFlags.Sampled | VkImageUsageFlags.TransferDst | VkImageUsageFlags.TransferSrc, + VkMemoryPropertyFlags.DeviceLocal, TEXTURE_DIM, TEXTURE_DIM, VkImageType.Image2D, + VkSampleCountFlags.SampleCount1, VkImageTiling.Optimal, Image.ComputeMipLevels (TEXTURE_DIM), ctx.ImageCount); + + ctx.BuildTexArray (ref texArray, 0); + + texArray.CreateView (VkImageViewType.ImageView2DArray, VkImageAspectFlags.Color, texArray.CreateInfo.arrayLayers); + texArray.CreateSampler (); + texArray.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + texArray.SetName ("model texArray"); + } + + loadMaterials (ctx); + materialUBO = new HostBuffer (dev, VkBufferUsageFlags.UniformBuffer, materials); + } + } + } + + void loadMaterials (glTFLoader ctx) { + glTFLoader.Material[] mats = ctx.LoadMaterial (); + materials = new Material[mats.Length]; + + for (int i = 0; i < mats.Length; i++) { + materials[i] = new Material { + workflow = (float)mats[i].workflow, + baseColorFactor = mats[i].baseColorFactor, + emissiveFactor = mats[i].emissiveFactor, + metallicFactor = mats[i].metallicFactor, + roughnessFactor = mats[i].roughnessFactor, + + baseColorTextureSet = mats[i].baseColorTexture, + phyDescTex = mats[i].metallicRoughnessTexture, + normalTex = mats[i].normalTexture, + aoTex = mats[i].occlusionTexture, + emissiveTex = mats[i].emissiveTexture, + + TexCoord0 = mats[i].availableAttachments, + TexCoord1 = mats[i].availableAttachments1, + + alphaMask = 0f, + alphaMaskCutoff = 0.0f, + diffuseFactor = new Vector4 (0), + specularFactor = new Vector4 (0) + }; + } + } + + public override void RenderNode (CommandBuffer cmd, PipelineLayout pipelineLayout, Node node, Matrix4x4 currentTransform, bool shadowPass = false) { + Matrix4x4 localMat = node.localMatrix * currentTransform; + + cmd.PushConstant (pipelineLayout, VkShaderStageFlags.Vertex, localMat); + + if (node.Mesh != null) { + foreach (Primitive p in node.Mesh.Primitives) { + if (!shadowPass) + cmd.PushConstant (pipelineLayout, VkShaderStageFlags.Fragment, (int)p.material, (uint)Marshal.SizeOf ()); + cmd.DrawIndexed (p.indexCount, 1, p.indexBase, p.vertexBase, 0); + } + } + if (node.Children == null) + return; + foreach (Node child in node.Children) + RenderNode (cmd, pipelineLayout, child, localMat, shadowPass); + } + + protected override void Dispose (bool disposing) { + if (!isDisposed) { + if (disposing) { + texArray?.Dispose (); + } else + Debug.WriteLine ("model was not disposed"); + } + base.Dispose (disposing); + } + } +} diff --git a/addons/gltfLoader/glTFLoader.cs b/addons/gltfLoader/glTFLoader.cs new file mode 100644 index 0000000..06e8ad6 --- /dev/null +++ b/addons/gltfLoader/glTFLoader.cs @@ -0,0 +1,635 @@ +// Copyright (c) 2019 Jean-Philippe Bruyère +// +// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) +using System; +using System.Diagnostics; +using System.Numerics; +using System.Runtime.InteropServices; +using glTFLoader; +using GL = glTFLoader.Schema; + +using VK; +using System.Collections.Generic; +using System.IO; + + + +namespace CVKL.glTF { + using static VK.Utils; + using static CVKL.Model; + + /// + /// Loading context with I as the vertex index type (uint16,uint32) + /// + public class glTFLoader : IDisposable { + /// + /// Material class used by the gltfLoader to fetch values. + /// + public class Material { + public enum Workflow { PhysicalyBaseRendering = 1, SpecularGlossinnes }; + public string Name; + public Workflow workflow; + public Int32 baseColorTexture; + public Int32 metallicRoughnessTexture; + public Int32 normalTexture; + public Int32 occlusionTexture; + public Int32 emissiveTexture; + + public Vector4 baseColorFactor; + public Vector4 emissiveFactor; + public AttachmentType availableAttachments; + public AttachmentType availableAttachments1; + + public AlphaMode alphaMode; + public float alphaCutoff; + public float metallicFactor; + public float roughnessFactor; + + public bool metallicRoughness = true; + public bool specularGlossiness = false; + + public Material (Int32 _baseColorTexture = -1, Int32 _metallicRoughnessTexture = -1, + Int32 _normalTexture = -1, Int32 _occlusionTexture = -1) { + workflow = Workflow.PhysicalyBaseRendering; + baseColorTexture = _baseColorTexture; + metallicRoughnessTexture = _metallicRoughnessTexture; + normalTexture = _normalTexture; + occlusionTexture = _occlusionTexture; + emissiveTexture = -1; + + alphaMode = AlphaMode.Opaque; + alphaCutoff = 1f; + metallicFactor = 1f; + roughnessFactor = 1; + baseColorFactor = new Vector4 (1); + emissiveFactor = new Vector4 (1); + + metallicRoughness = true; + specularGlossiness = false; + + } + } + + public Queue transferQ; + public CommandPool cmdPool; + Device dev => transferQ.Dev; + + public GL.Gltf gltf; + public string baseDirectory; + + public byte[][] loadedBuffers; + public GCHandle[] bufferHandles; + + List meshes; + string path; + + public glTFLoader (string path, Queue _transferQ, CommandPool _cmdPool) { + this.path = path; + transferQ = _transferQ; + cmdPool = _cmdPool; + baseDirectory = System.IO.Path.GetDirectoryName (path); + gltf = Interface.LoadModel (path); ; + loadedBuffers = new byte[gltf.Buffers.Length][]; + bufferHandles = new GCHandle[gltf.Buffers.Length]; + } + + static byte[] loadDataUri (GL.Image img) { + int idxComa = img.Uri.IndexOf (",", 5, StringComparison.Ordinal); + return Convert.FromBase64String (img.Uri.Substring (idxComa + 1)); + } + static byte[] loadDataUri (GL.Buffer buff) { + int idxComa = buff.Uri.IndexOf (",", 5, StringComparison.Ordinal); + return Convert.FromBase64String (buff.Uri.Substring (idxComa + 1)); + } + + void EnsureBufferIsLoaded (int bufferIdx) { + if (loadedBuffers[bufferIdx] == null) { + //load full buffer + string uri = gltf.Buffers[bufferIdx].Uri; + if (string.IsNullOrEmpty(uri))//glb + loadedBuffers[bufferIdx] = gltf.LoadBinaryBuffer (bufferIdx, path); + else if (uri.StartsWith ("data", StringComparison.Ordinal)) + loadedBuffers[bufferIdx] = loadDataUri (gltf.Buffers[bufferIdx]);//TODO:check this func=>System.Buffers.Text.Base64.EncodeToUtf8InPlace + else + loadedBuffers[bufferIdx] = File.ReadAllBytes (Path.Combine (baseDirectory, gltf.Buffers[bufferIdx].Uri)); + bufferHandles[bufferIdx] = GCHandle.Alloc (loadedBuffers[bufferIdx], GCHandleType.Pinned); + } + } + + public void GetVertexCount (out ulong vertexCount, out ulong indexCount, out VkIndexType largestIndexType) { + vertexCount = 0; + indexCount = 0; + largestIndexType = VkIndexType.Uint16; + //compute size of stagging buf + foreach (GL.Mesh mesh in gltf.Meshes) { + foreach (GL.MeshPrimitive p in mesh.Primitives) { + int accessorIdx; + if (p.Attributes.TryGetValue ("POSITION", out accessorIdx)) + vertexCount += (ulong)gltf.Accessors[accessorIdx].Count; + if (p.Indices != null) { + indexCount += (ulong)gltf.Accessors[(int)p.Indices].Count; + if (gltf.Accessors[(int)p.Indices].ComponentType == GL.Accessor.ComponentTypeEnum.UNSIGNED_INT) + largestIndexType = VkIndexType.Uint32; + } + } + } + } + + public uint ImageCount => gltf.Images == null ? 0 : (uint)gltf.Images.Length; + + + //TODO: some buffer data are reused between primitives, and I duplicate the datas + //buffers must be constructed without duplications + public Mesh[] LoadMeshes (VkIndexType indexType, Buffer vbo, ulong vboOffset, Buffer ibo, ulong iboOffset) { + ulong vCount, iCount; + VkIndexType idxType; + GetVertexCount (out vCount, out iCount, out idxType); + + int vertexByteSize = Marshal.SizeOf (); + ulong vertSize = vCount * (ulong)vertexByteSize; + ulong idxSize = iCount * (indexType == VkIndexType.Uint16 ? 2ul : 4ul); + ulong size = vertSize + idxSize; + + int vertexCount = 0, indexCount = 0; + int autoNamedMesh = 1; + + meshes = new List (); + + using (HostBuffer stagging = new HostBuffer (dev, VkBufferUsageFlags.TransferSrc, size)) { + stagging.Map (); + + unsafe { + byte* stagVertPtrInit = (byte*)stagging.MappedData.ToPointer (); + byte* stagIdxPtrInit = (byte*)(stagging.MappedData.ToPointer ()) + vertSize; + byte* stagVertPtr = stagVertPtrInit; + byte* stagIdxPtr = stagIdxPtrInit; + + foreach (GL.Mesh mesh in gltf.Meshes) { + + string meshName = mesh.Name; + if (string.IsNullOrEmpty (meshName)) { + meshName = "mesh_" + autoNamedMesh.ToString (); + autoNamedMesh++; + } + Mesh m = new Mesh { Name = meshName }; + + foreach (GL.MeshPrimitive p in mesh.Primitives) { + GL.Accessor AccPos = null, AccNorm = null, AccUv = null, AccUv1 = null; + + int accessorIdx; + if (p.Attributes.TryGetValue ("POSITION", out accessorIdx)) { + AccPos = gltf.Accessors[accessorIdx]; + EnsureBufferIsLoaded (gltf.BufferViews[(int)AccPos.BufferView].Buffer); + } + if (p.Attributes.TryGetValue ("NORMAL", out accessorIdx)) { + AccNorm = gltf.Accessors[accessorIdx]; + EnsureBufferIsLoaded (gltf.BufferViews[(int)AccNorm.BufferView].Buffer); + } + if (p.Attributes.TryGetValue ("TEXCOORD_0", out accessorIdx)) { + AccUv = gltf.Accessors[accessorIdx]; + EnsureBufferIsLoaded (gltf.BufferViews[(int)AccUv.BufferView].Buffer); + } + if (p.Attributes.TryGetValue ("TEXCOORD_1", out accessorIdx)) { + AccUv1 = gltf.Accessors[accessorIdx]; + EnsureBufferIsLoaded (gltf.BufferViews[(int)AccUv1.BufferView].Buffer); + } + + Primitive prim = new Primitive { + indexBase = (uint)indexCount, + vertexBase = vertexCount, + vertexCount = (uint)AccPos.Count, + material = (uint)(p.Material ?? 0) + }; + + prim.bb.min.ImportFloatArray (AccPos.Min); + prim.bb.max.ImportFloatArray (AccPos.Max); + prim.bb.isValid = true; + + //Interleaving vertices + byte * inPosPtr = null, inNormPtr = null, inUvPtr = null, inUv1Ptr = null; + + GL.BufferView bv = gltf.BufferViews[(int)AccPos.BufferView]; + inPosPtr = (byte*)bufferHandles[bv.Buffer].AddrOfPinnedObject ().ToPointer (); + inPosPtr += AccPos.ByteOffset + bv.ByteOffset; + + if (AccNorm != null) { + bv = gltf.BufferViews[(int)AccNorm.BufferView]; + inNormPtr = (byte*)bufferHandles[bv.Buffer].AddrOfPinnedObject ().ToPointer (); + inNormPtr += AccNorm.ByteOffset + bv.ByteOffset; + } + if (AccUv != null) { + bv = gltf.BufferViews[(int)AccUv.BufferView]; + inUvPtr = (byte*)bufferHandles[bv.Buffer].AddrOfPinnedObject ().ToPointer (); + inUvPtr += AccUv.ByteOffset + bv.ByteOffset; + } + if (AccUv1 != null) { + bv = gltf.BufferViews[(int)AccUv1.BufferView]; + inUv1Ptr = (byte*)bufferHandles[bv.Buffer].AddrOfPinnedObject ().ToPointer (); + inUv1Ptr += AccUv1.ByteOffset + bv.ByteOffset; + } + + + for (int j = 0; j < prim.vertexCount; j++) { + System.Buffer.MemoryCopy (inPosPtr, stagVertPtr, 12, 12); + inPosPtr += 12; + if (inNormPtr != null) { + System.Buffer.MemoryCopy (inNormPtr, stagVertPtr + 12, 12, 12); + inNormPtr += 12; + } + if (inUvPtr != null) { + System.Buffer.MemoryCopy (inUvPtr, stagVertPtr + 24, 8, 8); + inUvPtr += 8; + } + if (inUv1Ptr != null) { + System.Buffer.MemoryCopy (inUv1Ptr, stagVertPtr + 32, 8, 8); + inUv1Ptr += 8; + } + stagVertPtr += vertexByteSize; + } + + //indices loading + if (p.Indices != null) { + GL.Accessor acc = gltf.Accessors[(int)p.Indices]; + bv = gltf.BufferViews[(int)acc.BufferView]; + + byte* inIdxPtr = (byte*)bufferHandles[bv.Buffer].AddrOfPinnedObject ().ToPointer (); + inIdxPtr += acc.ByteOffset + bv.ByteOffset; + + //TODO:double check this, I dont seems to increment stag pointer + if (acc.ComponentType == GL.Accessor.ComponentTypeEnum.UNSIGNED_SHORT) { + if (indexType == VkIndexType.Uint16) { + System.Buffer.MemoryCopy (inIdxPtr, stagIdxPtr, (long)acc.Count * 2, (long)acc.Count * 2); + stagIdxPtr += (long)acc.Count * 2; + } else { + uint* usPtr = (uint*)stagIdxPtr; + ushort* inPtr = (ushort*)inIdxPtr; + for (int i = 0; i < acc.Count; i++) + usPtr[i] = inPtr[i]; + stagIdxPtr += (long)acc.Count * 4; + } + } else if (acc.ComponentType == GL.Accessor.ComponentTypeEnum.UNSIGNED_INT) { + if (indexType == VkIndexType.Uint32) { + System.Buffer.MemoryCopy (inIdxPtr, stagIdxPtr, (long)acc.Count * 4, (long)acc.Count * 4); + stagIdxPtr += (long)acc.Count * 4; + } else { + ushort* usPtr = (ushort*)stagIdxPtr; + uint* inPtr = (uint*)inIdxPtr; + for (int i = 0; i < acc.Count; i++) + usPtr[i] = (ushort)inPtr[i]; + stagIdxPtr += (long)acc.Count * 2; + } + } else if (acc.ComponentType == GL.Accessor.ComponentTypeEnum.UNSIGNED_BYTE) { + //convert + if (indexType == VkIndexType.Uint16) { + ushort* usPtr = (ushort*)stagIdxPtr; + for (int i = 0; i < acc.Count; i++) + usPtr[i] = (ushort)inIdxPtr[i]; + stagIdxPtr += (long)acc.Count * 2; + } else { + uint* usPtr = (uint*)stagIdxPtr; + for (int i = 0; i < acc.Count; i++) + usPtr[i] = (uint)inIdxPtr[i]; + stagIdxPtr += (long)acc.Count * 4; + } + } else + throw new NotImplementedException (); + + prim.indexCount = (uint)acc.Count; + indexCount += acc.Count; + } + + m.AddPrimitive (prim); + + vertexCount += AccPos.Count; + } + meshes.Add (m); + } + } + + stagging.Unmap (); + + CommandBuffer cmd = cmdPool.AllocateCommandBuffer (); + cmd.Start (VkCommandBufferUsageFlags.OneTimeSubmit); + + stagging.CopyTo (cmd, vbo, vertSize, 0, vboOffset); + if (iCount>0) + stagging.CopyTo (cmd, ibo, idxSize, vertSize, iboOffset); + + cmd.End (); + + transferQ.Submit (cmd); + + dev.WaitIdle (); + cmd.Free (); + + } + + return meshes.ToArray (); + } + + public Scene[] LoadScenes (out int defaultScene) { + defaultScene = -1; + if (gltf.Scene == null) + return new Scene[] {}; + + List scenes = new List (); + defaultScene = (int)gltf.Scene; + + for (int i = 0; i < gltf.Scenes.Length; i++) { + GL.Scene scene = gltf.Scenes[i]; + Debug.WriteLine ("Loading Scene {0}", scene.Name); + + scenes.Add (new Scene { + Name = scene.Name, + }); + + if (scene.Nodes.Length == 0) + continue; + + scenes[i].Root = new Node { + localMatrix = Matrix4x4.Identity, + Children = new List () + }; + + foreach (int nodeIdx in scene.Nodes) + loadNode (scenes[i].Root, gltf.Nodes[nodeIdx]); + } + return scenes.ToArray (); + } + + void loadNode (Node parentNode, GL.Node gltfNode) { + Debug.WriteLine ("Loading node {0}", gltfNode.Name); + + Vector3 translation = new Vector3 (); + Quaternion rotation = Quaternion.Identity; + Vector3 scale = new Vector3 (1); + Matrix4x4 localTransform = Matrix4x4.Identity; + + if (gltfNode.Matrix != null) { + float[] M = gltfNode.Matrix; + localTransform = new Matrix4x4 ( + M[0], M[1], M[2], M[3], + M[4], M[5], M[6], M[7], + M[8], M[9],M[10],M[11], + M[12],M[13],M[14],M[15]); + } + + if (gltfNode.Translation != null) + FromFloatArray (ref translation, gltfNode.Translation); + if (gltfNode.Translation != null) + FromFloatArray (ref rotation, gltfNode.Rotation); + if (gltfNode.Translation != null) + FromFloatArray (ref scale, gltfNode.Scale); + + localTransform *= + Matrix4x4.CreateScale (scale) * + Matrix4x4.CreateFromQuaternion (rotation) * + Matrix4x4.CreateTranslation (translation); + + //localTransform = Matrix4x4.Identity; + + Node node = new Node { + localMatrix = localTransform, + Parent = parentNode, + Name = gltfNode.Name + }; + parentNode.Children.Add (node); + + if (gltfNode.Children != null) { + node.Children = new List (); + for (int i = 0; i < gltfNode.Children.Length; i++) + loadNode (node, gltf.Nodes[gltfNode.Children[i]]); + } + + if (gltfNode.Mesh != null) + node.Mesh = meshes[(int)gltfNode.Mesh]; + } + + ///// + ///// build texture array + ///// + ///// The images. + ///// Uniformized Texture size for all images + public void BuildTexArray (ref Image texArray, uint firstImg = 0) { + int texDim = (int)texArray.CreateInfo.extent.width; + + CommandBuffer cmd = cmdPool.AllocateAndStart (VkCommandBufferUsageFlags.OneTimeSubmit); + texArray.SetLayout (cmd, VkImageAspectFlags.Color, VkImageLayout.Undefined, VkImageLayout.TransferDstOptimal, + VkPipelineStageFlags.BottomOfPipe, VkPipelineStageFlags.Transfer); + cmd.End (); + transferQ.Submit (cmd); + transferQ.WaitIdle (); + cmd.Free (); + + VkImageBlit imageBlit = new VkImageBlit { + srcSubresource = new VkImageSubresourceLayers (VkImageAspectFlags.Color, 1, 0), + dstOffsets_1 = new VkOffset3D (texDim, texDim, 1) + }; + + for (int l = 0; l < gltf.Images.Length; l++) { + GL.Image img = gltf.Images[l]; + Image vkimg = null; + + if (img.BufferView != null) {//load image from gltf buffer view + GL.BufferView bv = gltf.BufferViews[(int)img.BufferView]; + EnsureBufferIsLoaded (bv.Buffer); + vkimg = Image.Load (dev, bufferHandles[bv.Buffer].AddrOfPinnedObject () + bv.ByteOffset, (ulong)bv.ByteLength, VkImageUsageFlags.TransferSrc); + } else if (img.Uri.StartsWith ("data:", StringComparison.Ordinal)) {//load base64 encoded image + Debug.WriteLine ("loading embedded image {0} : {1}", img.Name, img.MimeType); + vkimg = Image.Load (dev, glTFLoader.loadDataUri (img), VkImageUsageFlags.TransferSrc); + } else { + Debug.WriteLine ("loading image {0} : {1} : {2}", img.Name, img.MimeType, img.Uri);//load image from file path in uri + vkimg = Image.Load (dev, Path.Combine (baseDirectory, img.Uri), VkImageUsageFlags.TransferSrc); + } + + imageBlit.srcOffsets_1 = new VkOffset3D ((int)vkimg.CreateInfo.extent.width, (int)vkimg.CreateInfo.extent.height, 1); + imageBlit.dstSubresource = new VkImageSubresourceLayers (VkImageAspectFlags.Color, 1, 0, (uint)l + firstImg); + + cmd = cmdPool.AllocateAndStart (VkCommandBufferUsageFlags.OneTimeSubmit); + + vkimg.SetLayout (cmd, VkImageAspectFlags.Color, + VkAccessFlags.HostWrite, VkAccessFlags.TransferRead, + VkImageLayout.Undefined, VkImageLayout.TransferSrcOptimal, + VkPipelineStageFlags.Host, VkPipelineStageFlags.Transfer); + + Vk.vkCmdBlitImage (cmd.Handle, vkimg.Handle, VkImageLayout.TransferSrcOptimal, + texArray.Handle, VkImageLayout.TransferDstOptimal, 1, ref imageBlit, VkFilter.Linear); + + cmd.End (); + transferQ.Submit (cmd); + transferQ.WaitIdle (); + cmd.Free (); + + vkimg.Dispose (); + } + + cmd = cmdPool.AllocateAndStart (VkCommandBufferUsageFlags.OneTimeSubmit); + + uint imgCount = (uint)gltf.Images.Length; + VkImageSubresourceRange mipSubRange = new VkImageSubresourceRange (VkImageAspectFlags.Color, 0, 1, firstImg, imgCount); + + for (int i = 1; i < texArray.CreateInfo.mipLevels; i++) { + imageBlit = new VkImageBlit { + srcSubresource = new VkImageSubresourceLayers (VkImageAspectFlags.Color, imgCount, (uint)i - 1, firstImg), + srcOffsets_1 = new VkOffset3D ((int)texDim >> (i - 1), (int)texDim >> (i - 1), 1), + dstSubresource = new VkImageSubresourceLayers (VkImageAspectFlags.Color, imgCount, (uint)i, firstImg), + dstOffsets_1 = new VkOffset3D ((int)texDim >> i, (int)texDim >> i, 1) + }; + + texArray.SetLayout (cmd, + VkAccessFlags.TransferWrite, VkAccessFlags.TransferRead, + VkImageLayout.TransferDstOptimal, VkImageLayout.TransferSrcOptimal, mipSubRange, + VkPipelineStageFlags.Transfer, VkPipelineStageFlags.Transfer); + + Vk.vkCmdBlitImage (cmd.Handle, texArray.Handle, VkImageLayout.TransferSrcOptimal, + texArray.Handle, VkImageLayout.TransferDstOptimal, 1, ref imageBlit, VkFilter.Linear); + texArray.SetLayout (cmd, VkImageLayout.TransferSrcOptimal, VkImageLayout.ShaderReadOnlyOptimal, mipSubRange, + VkPipelineStageFlags.Transfer, VkPipelineStageFlags.FragmentShader); + + mipSubRange.baseMipLevel = (uint)i; + } + mipSubRange.baseMipLevel = texArray.CreateInfo.mipLevels - 1; + texArray.SetLayout (cmd, VkImageLayout.TransferDstOptimal, VkImageLayout.ShaderReadOnlyOptimal, mipSubRange, + VkPipelineStageFlags.Transfer, VkPipelineStageFlags.FragmentShader); + + cmd.End (); + transferQ.Submit (cmd); + transferQ.WaitIdle (); + cmd.Free (); + } + /// + /// Load model images as separate texture in a c# array + /// + /// The images. + public Image[] LoadImages () { + if (gltf.Images == null) + return new Image[] {}; + + List textures = new List (); + + foreach (GL.Image img in gltf.Images) { + Image vkimg = null; + + string imgName = img.Name; + + if (img.BufferView != null) {//load image from gltf buffer view + GL.BufferView bv = gltf.BufferViews[(int)img.BufferView]; + EnsureBufferIsLoaded (bv.Buffer); + vkimg = Image.Load (dev, transferQ, cmdPool, bufferHandles[bv.Buffer].AddrOfPinnedObject () + bv.ByteOffset, (ulong)bv.ByteLength); + } else if (img.Uri.StartsWith ("data:", StringComparison.Ordinal)) {//load base64 encoded image + Debug.WriteLine ("loading embedded image {0} : {1}", img.Name, img.MimeType); + vkimg = Image.Load (dev, transferQ, cmdPool, glTFLoader.loadDataUri (img)); + } else { + Debug.WriteLine ("loading image {0} : {1} : {2}", img.Name, img.MimeType, img.Uri);//load image from file path in uri + vkimg = Image.Load (dev, transferQ, cmdPool, Path.Combine (baseDirectory, img.Uri)); + imgName += ";" + img.Uri; + } + + vkimg.CreateView (); + vkimg.CreateSampler (); + vkimg.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + + vkimg.SetName (imgName); + vkimg.Descriptor.imageView.SetDebugMarkerName (dev, "imgView " + imgName); + vkimg.Descriptor.sampler.SetDebugMarkerName (dev, "sampler " + imgName); + + textures.Add (vkimg); + } + return textures.ToArray (); + } + + public Material[] LoadMaterial () { + if (gltf.Materials == null) + return new Material[] {}; + + List materials = new List (); + + foreach (GL.Material mat in gltf.Materials) { + Debug.WriteLine ("loading material: " + mat.Name); + Material pbr = new Material (); + pbr.Name = mat.Name; + + pbr.alphaCutoff = mat.AlphaCutoff; + pbr.alphaMode = (AlphaMode)mat.AlphaMode; + + FromFloatArray (ref pbr.emissiveFactor, mat.EmissiveFactor); + + if (mat.EmissiveTexture != null) { + pbr.emissiveTexture = mat.EmissiveTexture.Index; + if (mat.EmissiveTexture.TexCoord == 1) + pbr.availableAttachments1 |= AttachmentType.Emissive; + else + pbr.availableAttachments |= AttachmentType.Emissive; + } + if (mat.NormalTexture != null) { + pbr.normalTexture = mat.NormalTexture.Index; + if (mat.NormalTexture.TexCoord == 1) + pbr.availableAttachments1 |= AttachmentType.Normal; + else + pbr.availableAttachments |= AttachmentType.Normal; + } + if (mat.OcclusionTexture != null) { + pbr.occlusionTexture = mat.OcclusionTexture.Index; + if (mat.OcclusionTexture.TexCoord == 1) + pbr.availableAttachments1 |= AttachmentType.AmbientOcclusion; + else + pbr.availableAttachments |= AttachmentType.AmbientOcclusion; + } + + if (mat.PbrMetallicRoughness != null) { + if (mat.PbrMetallicRoughness.BaseColorTexture != null) { + pbr.baseColorTexture = mat.PbrMetallicRoughness.BaseColorTexture.Index; + if (mat.PbrMetallicRoughness.BaseColorTexture.TexCoord == 1) + pbr.availableAttachments1 |= AttachmentType.Color; + else + pbr.availableAttachments |= AttachmentType.Color; + } + + FromFloatArray (ref pbr.baseColorFactor, mat.PbrMetallicRoughness.BaseColorFactor); + + if (mat.PbrMetallicRoughness.MetallicRoughnessTexture != null) { + pbr.metallicRoughnessTexture = mat.PbrMetallicRoughness.MetallicRoughnessTexture.Index; + if (mat.PbrMetallicRoughness.MetallicRoughnessTexture.TexCoord == 1) + pbr.availableAttachments1 |= AttachmentType.PhysicalProps; + else + pbr.availableAttachments |= AttachmentType.PhysicalProps; + } + pbr.metallicFactor = mat.PbrMetallicRoughness.MetallicFactor; + pbr.roughnessFactor = mat.PbrMetallicRoughness.RoughnessFactor; + + pbr.workflow = Material.Workflow.PhysicalyBaseRendering; + } + materials.Add (pbr); + } + return materials.ToArray (); + } + + + #region IDisposable Support + private bool isDisposed = false; // Pour détecter les appels redondants + + protected virtual void Dispose (bool disposing) { + if (!isDisposed) { + if (disposing) { + // TODO: supprimer l'état managé (objets managés). + } + + for (int i = 0; i < gltf.Buffers.Length; i++) { + if (bufferHandles[i].IsAllocated) + bufferHandles[i].Free (); + } + + isDisposed = true; + } + } + + ~glTFLoader () { + Dispose (false); + } + public void Dispose () { + Dispose (true); + GC.SuppressFinalize (this); + } + #endregion + } +} \ No newline at end of file diff --git a/addons/gltfLoader/gltfLoader.csproj b/addons/gltfLoader/gltfLoader.csproj new file mode 100644 index 0000000..a439773 --- /dev/null +++ b/addons/gltfLoader/gltfLoader.csproj @@ -0,0 +1,21 @@ + + + + CVKL.gltfLoader + CVKL.gltfLoader + 0.1.6 + CVKL gltf addons + C# vulkan CVKL gltf + $(AssemblyVersion)-beta + True + False + https://github.com/jpbruyere/vk.net/blob/master/README.md + MIT + + false + true + + + + + diff --git a/clean.sh b/clean.sh new file mode 100755 index 0000000..d39df8f --- /dev/null +++ b/clean.sh @@ -0,0 +1,2 @@ +#!/bin/bash +rm -fr build && find . -iname bin -o -iname obj | xargs rm -rf diff --git a/netfx.props b/netfx.props new file mode 100644 index 0000000..25fdbe0 --- /dev/null +++ b/netfx.props @@ -0,0 +1,27 @@ + + + + + true + + + /Library/Frameworks/Mono.framework/Versions/Current/lib/mono + /usr/lib/mono + /usr/local/lib/mono + + + $(BaseFrameworkPathOverrideForMono)/4.5-api + $(BaseFrameworkPathOverrideForMono)/4.5.1-api + $(BaseFrameworkPathOverrideForMono)/4.5.2-api + $(BaseFrameworkPathOverrideForMono)/4.6-api + $(BaseFrameworkPathOverrideForMono)/4.6.1-api + $(BaseFrameworkPathOverrideForMono)/4.6.2-api + $(BaseFrameworkPathOverrideForMono)/4.7-api + $(BaseFrameworkPathOverrideForMono)/4.7.1-api + true + + + $(FrameworkPathOverride)/Facades;$(AssemblySearchPaths) + + + diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props new file mode 100644 index 0000000..738ae0b --- /dev/null +++ b/samples/Directory.Build.props @@ -0,0 +1,44 @@ + + + $(MSBuildThisFileDirectory)../ + true + + net471;netstandard2.0 + + https://github.com/jpbruyere/vke.net + MIT + Jean-Philippe Bruyère + + $(SolutionDir)build\$(Configuration)\ + Exe + true + false + + $(MSBuildThisFileDirectory)common\shaders + + + + + TRACE;DEBUG;NETSTANDARD;NETSTANDARD2_0;_WITH_SHADOWS;WITH_VKVG + + + NETSTANDARD;NETSTANDARD2_0;WITH_SHADOWS;_WITH_VKVG + + + + + + + + + + + + + + + + + + + diff --git a/samples/DistanceFieldFontTest/DistanceFieldFontTest.csproj b/samples/DistanceFieldFontTest/DistanceFieldFontTest.csproj new file mode 100644 index 0000000..c603527 --- /dev/null +++ b/samples/DistanceFieldFontTest/DistanceFieldFontTest.csproj @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/samples/DistanceFieldFontTest/Program.cs b/samples/DistanceFieldFontTest/Program.cs new file mode 100644 index 0000000..1eea932 --- /dev/null +++ b/samples/DistanceFieldFontTest/Program.cs @@ -0,0 +1,300 @@ +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.InteropServices; +using Glfw; + +using VK; +using CVKL; +using CVKL.DistanceFieldFont; + +namespace DistanceFieldFontTest { + + + + class Program : VkWindow { + static void Main (string[] args) { +#if DEBUG + Instance.VALIDATION = true; + Instance.DEBUG_UTILS = true; + Instance.RENDER_DOC_CAPTURE = false; +#endif + using (Program vke = new Program ()) { + vke.Run (); + } + } + + float rotSpeed = 0.01f, zoomSpeed = 0.01f; + float rotX, rotY, rotZ = 0f, zoom = 1f; + + struct Matrices { + public Matrix4x4 projection; + public Matrix4x4 view; + public Matrix4x4 model; + } + + Matrices matrices; + + HostBuffer uboMats; + GPUBuffer vbo; + GPUBuffer ibo; + + DescriptorPool descriptorPool; + DescriptorSetLayout dsLayout; + DescriptorSet descriptorSet; + + GraphicPipeline pipeline; + Framebuffer[] frameBuffers; + + Image fontTexture; + + struct Vertex { + public Vector3 pos; + public Vector2 uv; + public Vertex (float x, float y, float z, float u, float v) { + pos.X = x; pos.Y = y; pos.Z = z; + uv.X = u; uv.Y = v; + } + } + + BMFont font; + Vector4 textColor = new Vector4 (1.0f, 1.0f, 0.0f, 1.0f);//alpha => 0:disabled 1:enabled + Vector4 outlineColor = new Vector4 (1.0f, 0.0f, 0.0f, 0.6f);//alpha => 0:disabled 1:enabled + + Program () : base () { + + font = new BMFont (Utils.DataDirectory + "font.fnt"); + + vbo = new GPUBuffer (dev, VkBufferUsageFlags.VertexBuffer, 1024); + ibo = new GPUBuffer (dev, VkBufferUsageFlags.IndexBuffer, 2048); + + descriptorPool = new DescriptorPool (dev, 1, + new VkDescriptorPoolSize (VkDescriptorType.UniformBuffer), + new VkDescriptorPoolSize (VkDescriptorType.CombinedImageSampler) + ); + + dsLayout = new DescriptorSetLayout (dev, 0, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Vertex, VkDescriptorType.UniformBuffer), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler)); + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, VkSampleCountFlags.SampleCount4, false); + + cfg.Layout = new PipelineLayout (dev, dsLayout); + cfg.Layout.AddPushConstants (new VkPushConstantRange (VkShaderStageFlags.Fragment, (uint)Marshal.SizeOf () * 2)); + + cfg.RenderPass = new RenderPass (dev, swapChain.ColorFormat, cfg.Samples); + + cfg.blendAttachments[0] = new VkPipelineColorBlendAttachmentState ( + true, VkBlendFactor.One, VkBlendFactor.OneMinusSrcAlpha, VkBlendOp.Add, VkBlendFactor.One, VkBlendFactor.Zero); + + cfg.AddVertexBinding (0, 5 * sizeof (float)); + cfg.AddVertexAttributes (0, VkFormat.R32g32b32Sfloat, VkFormat.R32g32Sfloat); + + cfg.AddShader (VkShaderStageFlags.Vertex, "#DistanceFieldFontTest.main.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "#DistanceFieldFontTest.main.frag.spv"); + + pipeline = new GraphicPipeline (cfg); + + uboMats = new HostBuffer (dev, VkBufferUsageFlags.UniformBuffer, matrices); + uboMats.Map ();//permanent map + + descriptorSet = descriptorPool.Allocate (dsLayout); + + fontTexture = font.GetPageTexture (0, presentQueue, cmdPool); + fontTexture.CreateView (); + fontTexture.CreateSampler (); + fontTexture.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + + DescriptorSetWrites dsUpdate = new DescriptorSetWrites (descriptorSet, dsLayout); + dsUpdate.Write (dev, uboMats.Descriptor, fontTexture.Descriptor); + + generateText ("Vulkan", out HostBuffer staggingVbo, out HostBuffer staggingIbo); + + CommandBuffer cmd = cmdPool.AllocateAndStart (VkCommandBufferUsageFlags.OneTimeSubmit); + + staggingVbo.CopyTo (cmd, vbo); + staggingIbo.CopyTo (cmd, ibo); + + presentQueue.EndSubmitAndWait (cmd); + + staggingVbo.Dispose (); + staggingIbo.Dispose (); + } + + + // Creates a vertex buffer containing quads for the passed text + void generateText (string text, out HostBuffer svbo, out HostBuffer sibo) { + List vertices = new List (); + List indices = new List (); + ushort indexOffset = 0; + + float w = fontTexture.Width; + + float posx = 0.0f; + float posy = 0.0f; + + for (int i = 0; i < text.Length; i++) { + BMChar charInfo = font.CharMap[text[i]]; + + + if (charInfo.width == 0) + charInfo.width = 36; + + float charw = ((float)(charInfo.width) / 36.0f); + float dimx = 1.0f * charw; + float charh = ((float)(charInfo.height) / 36.0f); + float dimy = 1.0f * charh; + posy = 1.0f - charh; + + float us = charInfo.x / w; + float ue = (charInfo.x + charInfo.width) / w; + float ts = charInfo.y / w; + float te = (charInfo.y + charInfo.height) / w; + + float xo = charInfo.xoffset / 36.0f; + float yo = charInfo.yoffset / 36.0f; + + vertices.Add (new Vertex (posx + dimx + xo, posy + dimy, 0.0f, ue, te)); + vertices.Add (new Vertex (posx + xo, posy + dimy, 0.0f, us, te)); + vertices.Add (new Vertex (posx + xo, posy, 0.0f, us, ts)); + vertices.Add (new Vertex (posx + dimx + xo, posy, 0.0f, ue, ts)); + + indices.AddRange (new ushort[] { indexOffset, (ushort)(indexOffset + 1), (ushort)(indexOffset + 2), (ushort)(indexOffset + 2), (ushort)(indexOffset + 3), indexOffset }); + indexOffset += 4; + + float advance = charInfo.xadvance / 36.0f; + posx += advance; + } + + Vertex[] vx = vertices.ToArray (); + + // Center + for (int i = 0; i < vx.Length; i++) { + vx[i].pos.X -= posx / 2.0f; + vx[i].pos.Y -= 0.5f; + } + + svbo = new HostBuffer (dev, VkBufferUsageFlags.TransferSrc, vx); + sibo = new HostBuffer (dev, VkBufferUsageFlags.TransferSrc, indices.ToArray()); + } + + bool rebuildBuffers; + + public override void Update () { + if (rebuildBuffers) { + buildCommandBuffers (); + rebuildBuffers = false; + } + } + + void buildCommandBuffers () { + for (int i = 0; i < swapChain.ImageCount; ++i) { + cmds[i]?.Free (); + cmds[i] = cmdPool.AllocateAndStart (); + + recordDraw (cmds[i], frameBuffers[i]); + + cmds[i].End (); + } + } + + void recordDraw (CommandBuffer cmd, Framebuffer fb) { + pipeline.RenderPass.Begin (cmd, fb); + + cmd.SetViewport (fb.Width, fb.Height); + cmd.SetScissor (fb.Width, fb.Height); + + cmd.BindPipeline (pipeline, descriptorSet); + + cmd.PushConstant (pipeline, textColor); + cmd.PushConstant (pipeline, outlineColor, 0, 16); + + cmd.BindVertexBuffer (vbo, 0); + cmd.BindIndexBuffer (ibo, VkIndexType.Uint16); + cmd.DrawIndexed ((uint)ibo.ElementCount,1,0,0,0); + + pipeline.RenderPass.End (cmd); + } + + + public override void UpdateView () { + matrices.projection = Matrix4x4.CreatePerspectiveFieldOfView (Utils.DegreesToRadians (60f), (float)swapChain.Width / (float)swapChain.Height, 0.1f, 256.0f); + matrices.view = Matrix4x4.CreateTranslation (0, 0, -2.5f * zoom); + matrices.model = + Matrix4x4.CreateFromAxisAngle (Vector3.UnitZ, rotZ) * + Matrix4x4.CreateFromAxisAngle (Vector3.UnitY, rotY) * + Matrix4x4.CreateFromAxisAngle (Vector3.UnitX, rotX); + + uboMats.Update (matrices, (uint)Marshal.SizeOf ()); + + updateViewRequested = false; + } + + protected override void onMouseMove (double xPos, double yPos) { + double diffX = lastMouseX - xPos; + double diffY = lastMouseY - yPos; + if (MouseButton[0]) { + rotY -= rotSpeed * (float)diffX; + rotX += rotSpeed * (float)diffY; + } else if (MouseButton[1]) { + zoom += zoomSpeed * (float)diffY; + } + + updateViewRequested = true; + } + + protected override void onKeyDown (Key key, int scanCode, Modifier modifiers) { + switch (key) { + case Key.F2: + if (modifiers.HasFlag (Modifier.Shift)) + outlineColor.W -= 0.01f; + else + outlineColor.W += 0.01f; + rebuildBuffers = true; + break; + default: + base.onKeyDown (key, scanCode, modifiers); + break; + } + } + protected override void OnResize () { + + updateViewRequested = true; + + if (frameBuffers != null) + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); + + frameBuffers = new Framebuffer[swapChain.ImageCount]; + + for (int i = 0; i < swapChain.ImageCount; ++i) { + frameBuffers[i] = new Framebuffer (pipeline.RenderPass, swapChain.Width, swapChain.Height, + (pipeline.Samples == VkSampleCountFlags.SampleCount1) ? new Image[] { + swapChain.images[i], + } : new Image[] { + null, + swapChain.images[i] + }); + } + + buildCommandBuffers (); + } + + protected override void Dispose (bool disposing) { + if (disposing) { + if (!isDisposed) { + dev.WaitIdle (); + pipeline.Dispose (); + dsLayout.Dispose (); + for (int i = 0; i < swapChain.ImageCount; i++) + frameBuffers[i].Dispose (); + descriptorPool.Dispose (); + fontTexture?.Dispose (); + vbo.Dispose (); + ibo.Dispose (); + uboMats.Dispose (); + } + } + base.Dispose (disposing); + } + } +} diff --git a/samples/DistanceFieldFontTest/shaders/main.frag b/samples/DistanceFieldFontTest/shaders/main.frag new file mode 100644 index 0000000..fa1f6c6 --- /dev/null +++ b/samples/DistanceFieldFontTest/shaders/main.frag @@ -0,0 +1,32 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (binding = 1) uniform sampler2D samplerColor; + +layout (push_constant) uniform PushConsts { + vec4 color; + vec4 outlineColor; +}; + +layout (location = 0) in vec2 inUV; +layout (location = 0) out vec4 outFragColor; + +void main() +{ + float distance = texture(samplerColor, inUV).a; + float smoothWidth = fwidth(distance); + float alpha = smoothstep(0.5 - smoothWidth, 0.5 + smoothWidth, distance); + vec3 rgb = color.rgb * vec3(alpha); + + if (outlineColor.a > 0.0) + { + float w = 1.0 - outlineColor.a; + alpha = smoothstep(w - smoothWidth, w + smoothWidth, distance); + rgb += mix(vec3(alpha), outlineColor.rgb, alpha); + } + + outFragColor = vec4(rgb, alpha * color.a); + +} \ No newline at end of file diff --git a/samples/DistanceFieldFontTest/shaders/main.vert b/samples/DistanceFieldFontTest/shaders/main.vert new file mode 100644 index 0000000..3b395bf --- /dev/null +++ b/samples/DistanceFieldFontTest/shaders/main.vert @@ -0,0 +1,28 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (location = 0) in vec3 inPos; +layout (location = 1) in vec3 inColor; + +layout (binding = 0) uniform UBO +{ + mat4 projectionMatrix; + mat4 viewMatrix; + mat4 modelMatrix; +} ubo; + +layout (location = 0) out vec3 outColor; + +out gl_PerVertex +{ + vec4 gl_Position; +}; + + +void main() +{ + outColor = inColor; + gl_Position = ubo.projectionMatrix * ubo.viewMatrix * ubo.modelMatrix * vec4(inPos.xyz, 1.0); +} diff --git a/samples/Model/Model.csproj b/samples/Model/Model.csproj new file mode 100644 index 0000000..d6da2bb --- /dev/null +++ b/samples/Model/Model.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/samples/Model/data/DamagedHelmet.bin b/samples/Model/data/DamagedHelmet.bin new file mode 100644 index 0000000000000000000000000000000000000000..662eacc1da580cdcbc08572d5535b3ed484b1579 GIT binary patch literal 558504 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
QfT0;l_#yBwVDH=Ps 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

FU^OH}aXK6cxj5!G(A6twqdkU|6*x_BaQz+Qj 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 literal 0 HcmV?d00001 diff --git a/samples/Model/data/DamagedHelmet.gltf b/samples/Model/data/DamagedHelmet.gltf new file mode 100644 index 0000000..d685d77 --- /dev/null +++ b/samples/Model/data/DamagedHelmet.gltf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efe99dfac198094a30c71dc02a4d3421f0eef6bf335aeb695daa4d62134cd93f +size 4537 diff --git a/samples/Model/main.cs b/samples/Model/main.cs new file mode 100644 index 0000000..6a98629 --- /dev/null +++ b/samples/Model/main.cs @@ -0,0 +1,257 @@ +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.InteropServices; +using CVKL; +using CVKL.glTF; +using VK; + +namespace ModelSample { + class Program : VkWindow { + static void Main (string[] args) { +#if DEBUG + Instance.VALIDATION = true; + Instance.DEBUG_UTILS = true; + Instance.RENDER_DOC_CAPTURE = false; +#endif + using (Program vke = new Program ()) { + vke.Run (); + } + } + + struct Matrices { + public Matrix4x4 projection; + public Matrix4x4 view; + public Matrix4x4 model; + //public Vector4 lightPos; + } + + public struct PushConstants { + public Matrix4x4 matrix; + } + + Matrices matrices; + + HostBuffer uboMats; + + DescriptorPool descriptorPool; + DescriptorSet dsMatrices, dsTextures; + DescriptorSetLayout descLayoutMatrix; + DescriptorSetLayout descLayoutTextures; + + Framebuffer[] frameBuffers; + + GraphicPipeline pipeline; + + VkSampleCountFlags NUM_SAMPLES = VkSampleCountFlags.SampleCount1; + + float rotSpeed = 0.01f, zoomSpeed = 0.01f; + float rotX, rotY, rotZ = 0f, zoom = 2f; + + SimpleModel helmet; + CVKL.DebugUtils.Messenger dbgmsg; + + Program () : base () { + + + descriptorPool = new DescriptorPool (dev, 2, + new VkDescriptorPoolSize (VkDescriptorType.UniformBuffer), + new VkDescriptorPoolSize (VkDescriptorType.CombinedImageSampler, 3)); + + descLayoutMatrix = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Vertex, VkDescriptorType.UniformBuffer)); + + descLayoutTextures = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (2, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler) + ); + + VkPushConstantRange pushConstantRange = new VkPushConstantRange { + stageFlags = VkShaderStageFlags.Vertex, + size = (uint)Marshal.SizeOf(), + offset = 0 + }; + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, NUM_SAMPLES); + cfg.rasterizationState.cullMode = VkCullModeFlags.Back; + if (NUM_SAMPLES != VkSampleCountFlags.SampleCount1) { + cfg.multisampleState.sampleShadingEnable = true; + cfg.multisampleState.minSampleShading = 0.5f; + } + + cfg.Layout = new PipelineLayout (dev, pushConstantRange, descLayoutMatrix, descLayoutTextures); + cfg.RenderPass = new RenderPass (dev, swapChain.ColorFormat, dev.GetSuitableDepthFormat (), cfg.Samples); + cfg.AddVertexBinding (0); + cfg.AddVertexAttributes (0, VkFormat.R32g32b32Sfloat, VkFormat.R32g32b32Sfloat, VkFormat.R32g32Sfloat); + + cfg.AddShader (VkShaderStageFlags.Vertex, "#Model.model.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "#Model.model.frag.spv"); + + pipeline = new GraphicPipeline (cfg); + + helmet = new SimpleModel (presentQueue, Utils.DataDirectory + "models/DamagedHelmet/glTF/DamagedHelmet.gltf"); + //helmet = new SimpleModel (presentQueue, Utils.DataDirectory + "models/Hubble.glb"); + + //helmet = new SimpleModel (presentQueue, "/mnt/devel/vulkan/Lugdunum/resources/models/Box.gltf"); + + dsMatrices = descriptorPool.Allocate (descLayoutMatrix); + dsTextures = descriptorPool.Allocate (descLayoutTextures); + + uboMats = new HostBuffer (dev, VkBufferUsageFlags.UniformBuffer, matrices); + //matrices.lightPos = new Vector4 (0.0f, 0.0f, -2.0f, 1.0f); + + DescriptorSetWrites uboUpdate = new DescriptorSetWrites (dsMatrices, descLayoutMatrix); + uboUpdate.Write (dev, uboMats.Descriptor); + + DescriptorSetWrites texturesUpdate = new DescriptorSetWrites (dsTextures, descLayoutTextures); + texturesUpdate.Write (dev, + helmet.textures[0].Descriptor, + helmet.textures[1].Descriptor, + helmet.textures[2].Descriptor); + + uboMats.Map ();//permanent map + } + + public override void UpdateView () { + matrices.projection = Matrix4x4.CreatePerspectiveFieldOfView (Utils.DegreesToRadians (45f), + (float)swapChain.Width / (float)swapChain.Height, 0.1f, 256.0f) * Camera.VKProjectionCorrection; + matrices.view = + Matrix4x4.CreateFromAxisAngle (Vector3.UnitZ, rotZ) * + Matrix4x4.CreateFromAxisAngle (Vector3.UnitY, rotY) * + Matrix4x4.CreateFromAxisAngle (Vector3.UnitX, rotX) * + Matrix4x4.CreateTranslation (0, 0, -3f * zoom); + matrices.model = Matrix4x4.Identity; + uboMats.Update (matrices, (uint)Marshal.SizeOf ()); + updateViewRequested = false; + } + + protected override void onMouseMove (double xPos, double yPos) { + double diffX = lastMouseX - xPos; + double diffY = lastMouseY - yPos; + if (MouseButton[0]) { + rotY -= rotSpeed * (float)diffX; + rotX += rotSpeed * (float)diffY; + } else if (MouseButton[1]) { + zoom += zoomSpeed * (float)diffY; + } + updateViewRequested = true; + } + void buildCommandBuffers () { + cmdPool.Reset (VkCommandPoolResetFlags.ReleaseResources); + cmds = cmdPool.AllocateCommandBuffer (swapChain.ImageCount); + + for (int i = 0; i < swapChain.ImageCount; ++i) { + Framebuffer fb = frameBuffers[i]; + cmds[i].Start (); + + pipeline.RenderPass.Begin (cmds[i], fb); + + cmds[i].SetViewport (swapChain.Width, swapChain.Height); + cmds[i].SetScissor (swapChain.Width, swapChain.Height); + + cmds[i].BindDescriptorSet (pipeline.Layout, dsMatrices); + cmds[i].BindDescriptorSet (pipeline.Layout, dsTextures, 1); + + PushConstants pc = new PushConstants { matrix = Matrix4x4.Identity }; + cmds[i].PushConstant (pipeline.Layout, VkShaderStageFlags.Vertex, pc, (uint)Marshal.SizeOf ()); + + cmds[i].BindPipeline (pipeline); + + helmet.DrawAll (cmds[i], pipeline.Layout); + + pipeline.RenderPass.End (cmds[i]); + + cmds[i].End (); + } + } + + protected override void OnResize () { + + if (frameBuffers != null) + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); + frameBuffers = new Framebuffer[swapChain.ImageCount]; + + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i] = new Framebuffer (pipeline.RenderPass, swapChain.Width, swapChain.Height, + (pipeline.Samples == VkSampleCountFlags.SampleCount1) ? new Image[] { + swapChain.images[i], + null + } : new Image[] { + null, + null, + swapChain.images[i] + }); + + buildCommandBuffers (); + } + + class SimpleModel : PbrModel { + public new struct Vertex { + [VertexAttribute (VertexAttributeType.Position, VkFormat.R32g32b32Sfloat)] + public Vector3 pos; + [VertexAttribute (VertexAttributeType.Normal, VkFormat.R32g32b32Sfloat)] + public Vector3 normal; + [VertexAttribute (VertexAttributeType.UVs, VkFormat.R32g32Sfloat)] + public Vector2 uv; + public override string ToString () { + return pos.ToString () + ";" + normal.ToString () + ";" + uv.ToString (); + } + }; + public Image[] textures; + + public SimpleModel (Queue transferQ, string path) { + dev = transferQ.Dev; + + using (CommandPool cmdPool = new CommandPool (dev, transferQ.index)) { + using (CVKL.glTF.glTFLoader ctx = new CVKL.glTF.glTFLoader(path, transferQ, cmdPool)) { + loadSolids (ctx); + textures = ctx.LoadImages (); + } + } + } + + public void DrawAll (CommandBuffer cmd, PipelineLayout pipelineLayout) { + //helmet.Meshes + cmd.BindVertexBuffer (vbo); + cmd.BindIndexBuffer (ibo, IndexBufferType); + foreach (Mesh m in Meshes) { + foreach (var p in m.Primitives) { + cmd.DrawIndexed (p.indexCount,1,p.indexBase,p.vertexBase); + } + } + + //foreach (Scene sc in Scenes) { + // foreach (Node node in sc.Root.Children) + // RenderNode (cmd, pipelineLayout, node, sc.Root.localMatrix, shadowPass); + //} + } + + public override void RenderNode (CommandBuffer cmd, PipelineLayout pipelineLayout, Node node, Matrix4x4 currentTransform, bool shadowPass = false) { + throw new System.NotImplementedException (); + } + } + + protected override void Dispose (bool disposing) { + if (disposing) { + if (!isDisposed) { + helmet.vbo.Dispose (); + helmet.ibo.Dispose (); + foreach (var t in helmet.textures) + t.Dispose (); + + pipeline.Dispose (); + descLayoutMatrix.Dispose (); + descLayoutTextures.Dispose (); + for (int i = 0; i < swapChain.ImageCount; i++) + frameBuffers[i]?.Dispose (); + descriptorPool.Dispose (); + uboMats.Dispose (); + dbgmsg?.Dispose (); + } + } + + base.Dispose (disposing); + } + } +} diff --git a/samples/Model/shaders/model.frag b/samples/Model/shaders/model.frag new file mode 100644 index 0000000..7083a06 --- /dev/null +++ b/samples/Model/shaders/model.frag @@ -0,0 +1,84 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (set = 1, binding = 0) uniform sampler2D samplerColor; +layout (set = 1, binding = 1) uniform sampler2D samplerNormal; +layout (set = 1, binding = 2) uniform sampler2D samplerOcclusion; + +layout (location = 0) in vec2 inUV; +layout (location = 1) in vec3 inN; +layout (location = 2) in vec3 inV;//ViewDir +layout (location = 3) in vec3 inL; + +layout (location = 0) out vec4 outFragColor; + + + +// http://www.thetenthplanet.de/archives/1180 +mat3 cotangent_frame(vec3 N, vec3 p, vec2 uv) +{ + // get edge vectors of the pixel triangle + vec3 dp1 = dFdx( p ); + vec3 dp2 = dFdy( p ); + vec2 duv1 = dFdx( uv ); + vec2 duv2 = dFdy( uv ); + + // solve the linear system + vec3 dp2perp = cross( dp2, N ); + vec3 dp1perp = cross( N, dp1 ); + vec3 T = dp2perp * duv1.x + dp1perp * duv2.x; + vec3 B = dp2perp * duv1.y + dp1perp * duv2.y; + + // construct a scale-invariant frame + float invmax = inversesqrt( max( dot(T,T), dot(B,B) ) ); + return mat3( T * invmax, B * invmax, N ); +} + +vec3 perturb_normal( vec3 N, vec3 V, vec2 texcoord ) +{ + // assume N, the interpolated vertex normal and + // V, the view vector (vertex to eye) + vec3 map = texture(samplerNormal, texcoord).xyz; + map = map * 255./127. - 128./127.; + mat3 TBN = cotangent_frame(N, -V, texcoord); + return normalize(TBN * map); +} + + +void main() +{ + vec4 color = texture(samplerColor, inUV); + + vec3 N = normalize(inN); + vec3 L = normalize(inL); + vec3 V = normalize(inV); + vec3 R = reflect(-L, N); + vec3 diffuse = max(dot(N, L), 0.0) * vec3(0.9); + vec3 specular = pow(max(dot(R, V), 0.0), 16.0) * vec3(0.75); + outFragColor = vec4(diffuse * color.rgb + specular, 1.0); +} +/*void main() +{ + vec4 diff = texture(samplerColor, inUV); + + vec3 n = normalize(inN); + vec3 l = normalize(-light); + vec3 pn = perturb_normal(n, inV, inUV); + + float lambert = max(0.0, dot(pn, l)); + + //diff.rgb *= lambert * texture(samplerOcclusion, inUV).rgb; + vec3 spec = vec3(0); + vec3 amb = vec3(0.1); + if (lambert >= 0.0) { + vec3 rd = reflect(l, pn); + float s = dot(rd, normalize(inV)); + spec = vec3(0.9,0.9,0.9)* //light.specular.xyz * material.specular.xyz * + pow(max(0.0, s), 10.5); //0.5 = mat shininess + } + //outFragColor = vec4(1.0); // + //outFragColor = vec4(diff.rgb , 1.0); // + outFragColor = vec4(diff.rgb + amb + spec , diff.a); // +}*/ \ No newline at end of file diff --git a/samples/Model/shaders/model.vert b/samples/Model/shaders/model.vert new file mode 100644 index 0000000..c4dbfb8 --- /dev/null +++ b/samples/Model/shaders/model.vert @@ -0,0 +1,50 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (location = 0) in vec3 inPos; +layout (location = 1) in vec3 inNormal; +layout (location = 2) in vec2 inUV; + +layout (binding = 0) uniform UBO +{ + mat4 projectionMatrix; + mat4 viewMatrix; + mat4 modelMatrix; +} ubo; + +layout (location = 0) out vec2 outUV; +layout (location = 1) out vec3 outN; +layout (location = 2) out vec3 outV;//ViewDir +layout (location = 3) out vec3 outL; + +out gl_PerVertex +{ + vec4 gl_Position; +}; + +layout(push_constant) uniform PushConsts { + mat4 model; +} pc; + +vec3 light = vec3(2.0,2.0,-2.0); + +void main() +{ + outUV = inUV; + + mat4 mod = ubo.modelMatrix;// * pc.model; + vec4 pos = mod * vec4(inPos.xyz, 1.0); + vec3 lPos = mat3(mod) * light; + + //outN = normalize(transpose(inverse(mat3(mod))) * inNormal); + outN = mat3(mod)* inNormal; + + //mat4 viewInv = inverse(ubo.viewMatrix); + + outV = -pos.xyz;//normalize(vec3(viewInv * vec4(0.0, 0.0, 0.0, 1.0) - pos)); + outL = lPos - pos.xyz; + + gl_Position = ubo.projectionMatrix * ubo.viewMatrix * pos; +} diff --git a/samples/Textured/Textured.csproj b/samples/Textured/Textured.csproj new file mode 100644 index 0000000..1785b4a --- /dev/null +++ b/samples/Textured/Textured.csproj @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/samples/Textured/main.cs b/samples/Textured/main.cs new file mode 100644 index 0000000..6b64eed --- /dev/null +++ b/samples/Textured/main.cs @@ -0,0 +1,266 @@ +using System; +using System.Numerics; +using System.Runtime.InteropServices; +using Glfw; +using CVKL; +using VK; + +namespace Textured { + class Program : VkWindow { + static void Main (string[] args) { +#if DEBUG + Instance.VALIDATION = true; + Instance.DEBUG_UTILS = true; + Instance.RENDER_DOC_CAPTURE = true; +#endif + + foreach (string s in System.Reflection.Assembly.GetEntryAssembly ().GetManifestResourceNames ()) + Console.WriteLine (s); + + using (Program vke = new Program ()) { + vke.Run (); + } + } + protected override void configureEnabledFeatures (VkPhysicalDeviceFeatures available_features, ref VkPhysicalDeviceFeatures features) { + base.configureEnabledFeatures (available_features, ref features); + features.textureCompressionBC = available_features.textureCompressionBC; + features.textureCompressionASTC_LDR = available_features.textureCompressionASTC_LDR; + } + + float rotSpeed = 0.01f, zoomSpeed = 0.01f; + float rotX, rotY, rotZ = 0f, zoom = 1f; + + struct Matrices { + public Matrix4x4 projection; + public Matrix4x4 view; + public Matrix4x4 model; + } + + Matrices matrices; + + HostBuffer uboMats; + GPUBuffer vbo; + GPUBuffer ibo; + + DescriptorPool descriptorPool; + DescriptorSetLayout dsLayout; + DescriptorSet descriptorSet; + + GraphicPipeline pipeline; + Framebuffer[] frameBuffers; + + Image texture; + Image nextTexture; + + float[] vertices = { + 1.0f, 1.0f, 0.0f , 1.0f, 0.0f, + -1.0f, 1.0f, 0.0f , 0.0f, 0.0f, + -1.0f, -1.0f, 0.0f , 0.0f, 1.0f, + 1.0f, -1.0f, 0.0f , 1.0f, 1.0f, + }; + ushort[] indices = { 0, 1, 2, 2, 0, 3 }; + int currentImgIndex = 0; + string[] imgPathes = { + "/mnt/devel/vulkan/VulkanExamples/data/models/voyager/voyager_rgba_unorm.ktx", + "/mnt/devel/vulkan/vulkanExUpstream/data/models/voyager/voyager_astc_8x8_unorm.ktx", + Utils.DataDirectory + "font.ktx", + "/mnt/devel/vulkan/vulkanExUpstream/data/textures/trail_astc_8x8_unorm.ktx", + Utils.DataDirectory + "textures/texturearray_rocks_bc3_unorm.ktx", + Utils.DataDirectory + "textures/texture.jpg", + Utils.DataDirectory + "textures/tex256.jpg", + }; + + Program () : base () { + + loadTexture (imgPathes[currentImgIndex]); + + vbo = new GPUBuffer (presentQueue, cmdPool, VkBufferUsageFlags.VertexBuffer, vertices); + ibo = new GPUBuffer (presentQueue, cmdPool, VkBufferUsageFlags.IndexBuffer, indices); + + descriptorPool = new DescriptorPool (dev, 1, + new VkDescriptorPoolSize (VkDescriptorType.UniformBuffer), + new VkDescriptorPoolSize (VkDescriptorType.CombinedImageSampler) + ); + + dsLayout = new DescriptorSetLayout (dev, 0, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Vertex, VkDescriptorType.UniformBuffer), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler)); + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, VkSampleCountFlags.SampleCount4); + + cfg.Layout = new PipelineLayout (dev, dsLayout); + cfg.RenderPass = new RenderPass (dev, swapChain.ColorFormat, dev.GetSuitableDepthFormat (), cfg.Samples); + + cfg.AddVertexBinding (0, 5 * sizeof(float)); + cfg.AddVertexAttributes (0, VkFormat.R32g32b32Sfloat, VkFormat.R32g32Sfloat); + + cfg.AddShader (VkShaderStageFlags.Vertex, "#Textured.main.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "#Textured.main.frag.spv"); + + pipeline = new GraphicPipeline (cfg); + + + uboMats = new HostBuffer (dev, VkBufferUsageFlags.UniformBuffer, matrices); + uboMats.Map ();//permanent map + + descriptorSet = descriptorPool.Allocate (dsLayout); + + updateTextureSet (); + + DescriptorSetWrites uboUpdate = new DescriptorSetWrites (descriptorSet, dsLayout.Bindings[0]); + uboUpdate.Write (dev, uboMats.Descriptor); + } + + void buildCommandBuffers () { + for (int i = 0; i < swapChain.ImageCount; ++i) { + cmds[i]?.Free (); + cmds[i] = cmdPool.AllocateAndStart (); + + recordDraw (cmds[i], frameBuffers[i]); + + cmds[i].End (); + } + } + void recordDraw (CommandBuffer cmd, Framebuffer fb) { + pipeline.RenderPass.Begin (cmd, fb); + + cmd.SetViewport (fb.Width, fb.Height); + cmd.SetScissor (fb.Width, fb.Height); + cmd.BindDescriptorSet (pipeline.Layout, descriptorSet); + + pipeline.Bind (cmd); + + cmd.BindVertexBuffer (vbo, 0); + cmd.BindIndexBuffer (ibo, VkIndexType.Uint16); + cmd.DrawIndexed ((uint)indices.Length); + + pipeline.RenderPass.End (cmd); + } + + VkMemoryPropertyFlags imgProp = VkMemoryPropertyFlags.DeviceLocal; + bool genMipMaps = true; + VkImageTiling tiling = VkImageTiling.Optimal; + + //in the thread of the keyboard + void loadTexture (string path) { + try { + Console.WriteLine ($"Loading:{path}"); + if (path.EndsWith ("ktx", StringComparison.OrdinalIgnoreCase)) + nextTexture = KTX.KTX.Load (presentQueue, cmdPool, path, + VkImageUsageFlags.Sampled, imgProp, genMipMaps, tiling); + else + nextTexture = Image.Load (dev, presentQueue, cmdPool, path, VkFormat.R8g8b8a8Unorm, imgProp, tiling, genMipMaps); + updateViewRequested = true; + } catch (Exception ex) { + Console.WriteLine (ex); + nextTexture = null; + } + } + + //in the main vulkan thread + void updateTextureSet (){ + nextTexture.CreateView (); + nextTexture.CreateSampler (); + nextTexture.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + + DescriptorSetWrites uboUpdate = new DescriptorSetWrites (descriptorSet, dsLayout.Bindings[1]); + uboUpdate.Write (dev, nextTexture.Descriptor); + + texture?.Dispose (); + texture = nextTexture; + nextTexture = null; + } + void updateMatrices () { + matrices.projection = Matrix4x4.CreatePerspectiveFieldOfView (Utils.DegreesToRadians (60f), (float)swapChain.Width / (float)swapChain.Height, 0.1f, 256.0f); + matrices.view = Matrix4x4.CreateTranslation (0, 0, -2.5f * zoom); + matrices.model = + Matrix4x4.CreateFromAxisAngle (Vector3.UnitZ, rotZ) * + Matrix4x4.CreateFromAxisAngle (Vector3.UnitY, rotY) * + Matrix4x4.CreateFromAxisAngle (Vector3.UnitX, rotX); + + uboMats.Update (matrices, (uint)Marshal.SizeOf ()); + } + + public override void UpdateView () { + if (nextTexture != null) { + updateTextureSet (); + buildCommandBuffers (); + }else + updateMatrices (); + + updateViewRequested = false; + } + + protected override void onMouseMove (double xPos, double yPos) { + double diffX = lastMouseX - xPos; + double diffY = lastMouseY - yPos; + if (MouseButton[0]) { + rotY -= rotSpeed * (float)diffX; + rotX += rotSpeed * (float)diffY; + } else if (MouseButton[1]) { + zoom += zoomSpeed * (float)diffY; + } + + updateViewRequested = true; + } + + protected override void onKeyDown (Key key, int scanCode, Modifier modifiers) { + switch (key) { + case Key.Space: + currentImgIndex++; + if (currentImgIndex == imgPathes.Length) + currentImgIndex = 0; + loadTexture (imgPathes[currentImgIndex]); + break; + default: + base.onKeyDown (key, scanCode, modifiers); + break; + } + } + + protected override void OnResize () { + + updateMatrices (); + + if (frameBuffers!=null) + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); + + frameBuffers = new Framebuffer[swapChain.ImageCount]; + + for (int i = 0; i < swapChain.ImageCount; ++i) { + frameBuffers[i] = new Framebuffer (pipeline.RenderPass, swapChain.Width, swapChain.Height, + (pipeline.Samples == VkSampleCountFlags.SampleCount1) ? new Image[] { + swapChain.images[i], + null + } : new Image[] { + null, + null, + swapChain.images[i] + }); + } + + buildCommandBuffers (); + } + + protected override void Dispose (bool disposing) { + if (disposing) { + if (!isDisposed) { + dev.WaitIdle (); + pipeline.Dispose (); + dsLayout.Dispose (); + for (int i = 0; i < swapChain.ImageCount; i++) + frameBuffers[i].Dispose (); + descriptorPool.Dispose (); + texture?.Dispose (); + nextTexture?.Dispose (); + vbo.Dispose (); + ibo.Dispose (); + uboMats.Dispose (); + } + } + + base.Dispose (disposing); + } + } +} diff --git a/samples/Textured/shaders/main.frag b/samples/Textured/shaders/main.frag new file mode 100644 index 0000000..73dfd79 --- /dev/null +++ b/samples/Textured/shaders/main.frag @@ -0,0 +1,15 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (binding = 1) uniform sampler2D samplerColor; + +layout (location = 0) in vec2 inUV; + +layout (location = 0) out vec4 outFragColor; + +void main() +{ + outFragColor = texture(samplerColor, inUV); +} \ No newline at end of file diff --git a/samples/Textured/shaders/main.vert b/samples/Textured/shaders/main.vert new file mode 100644 index 0000000..3b395bf --- /dev/null +++ b/samples/Textured/shaders/main.vert @@ -0,0 +1,28 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (location = 0) in vec3 inPos; +layout (location = 1) in vec3 inColor; + +layout (binding = 0) uniform UBO +{ + mat4 projectionMatrix; + mat4 viewMatrix; + mat4 modelMatrix; +} ubo; + +layout (location = 0) out vec3 outColor; + +out gl_PerVertex +{ + vec4 gl_Position; +}; + + +void main() +{ + outColor = inColor; + gl_Position = ubo.projectionMatrix * ubo.viewMatrix * ubo.modelMatrix * vec4(inPos.xyz, 1.0); +} diff --git a/samples/TexturedCube/TexturedCube.csproj b/samples/TexturedCube/TexturedCube.csproj new file mode 100644 index 0000000..8235ef1 --- /dev/null +++ b/samples/TexturedCube/TexturedCube.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/samples/TexturedCube/main.cs b/samples/TexturedCube/main.cs new file mode 100644 index 0000000..21aef33 --- /dev/null +++ b/samples/TexturedCube/main.cs @@ -0,0 +1,330 @@ +// Copyright (c) 2019 Jean-Philippe Bruyère +// +// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) +using System; +using System.Numerics; +using System.Runtime.InteropServices; +using Glfw; +using CVKL; +using VK; +using Buffer = CVKL.Buffer; + +namespace TextureCube { + class Program : VkWindow { + static void Main (string[] args) { +#if DEBUG + Instance.VALIDATION = true; + Instance.DEBUG_UTILS = true; + Instance.RENDER_DOC_CAPTURE = false; +#endif + using (Program vke = new Program ()) { + vke.Run (); + } + } + + float rotSpeed = 0.01f, zoomSpeed = 0.01f; + float rotX, rotY, rotZ = 0f, zoom = 1f; + + struct Matrices { + public Matrix4x4 projection; + public Matrix4x4 view; + } + + Matrices matrices; + + HostBuffer uboMats; + GPUBuffer vbo; + DescriptorPool descriptorPool; + DescriptorSetLayout dsLayout; + DescriptorSet descriptorSet, dsVkvg; + GraphicPipeline pipeline; + Framebuffer[] frameBuffers; + + Image texture; + Image nextTexture; + + static float[] g_vertex_buffer_data = { + -1.0f,-1.0f,-1.0f, 0.0f, 1.0f, // -X side + -1.0f,-1.0f, 1.0f, 1.0f, 1.0f, + -1.0f, 1.0f, 1.0f, 1.0f, 0.0f, + -1.0f, 1.0f, 1.0f, 1.0f, 0.0f, + -1.0f, 1.0f,-1.0f, 0.0f, 0.0f, + -1.0f,-1.0f,-1.0f, 0.0f, 1.0f, + + -1.0f,-1.0f,-1.0f, 1.0f, 1.0f, // -Z side + 1.0f, 1.0f,-1.0f, 0.0f, 0.0f, + 1.0f,-1.0f,-1.0f, 0.0f, 1.0f, + -1.0f,-1.0f,-1.0f, 1.0f, 1.0f, + -1.0f, 1.0f,-1.0f, 1.0f, 0.0f, + 1.0f, 1.0f,-1.0f, 0.0f, 0.0f, + + -1.0f,-1.0f,-1.0f, 1.0f, 0.0f, // -Y side + 1.0f,-1.0f,-1.0f, 1.0f, 1.0f, + 1.0f,-1.0f, 1.0f, 0.0f, 1.0f, + -1.0f,-1.0f,-1.0f, 1.0f, 0.0f, + 1.0f,-1.0f, 1.0f, 0.0f, 1.0f, + -1.0f,-1.0f, 1.0f, 0.0f, 0.0f, + + -1.0f, 1.0f,-1.0f, 1.0f, 0.0f, // +Y side + -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, + -1.0f, 1.0f,-1.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 1.0f,-1.0f, 1.0f, 1.0f, + + 1.0f, 1.0f,-1.0f, 1.0f, 0.0f, // +X side + 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, + 1.0f,-1.0f, 1.0f, 0.0f, 1.0f, + 1.0f,-1.0f, 1.0f, 0.0f, 1.0f, + 1.0f,-1.0f,-1.0f, 1.0f, 1.0f, + 1.0f, 1.0f,-1.0f, 1.0f, 0.0f, + + -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, // +Z side + -1.0f,-1.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, + -1.0f,-1.0f, 1.0f, 0.0f, 1.0f, + 1.0f,-1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, + }; + int currentImgIndex = 4; + string[] imgPathes = { + Utils.DataDirectory + "textures/uffizi_cube.ktx", + Utils.DataDirectory + "textures/papermill.ktx", + Utils.DataDirectory + "textures/cubemap_yokohama_bc3_unorm.ktx", + Utils.DataDirectory + "textures/gcanyon_cube.ktx", + Utils.DataDirectory + "textures/pisa_cube.ktx", + }; + + VkSampleCountFlags samples = VkSampleCountFlags.SampleCount1; + +#if WITH_VKVG + VkvgPipeline.VkvgPipeline vkvgPipeline; + + void vkvgDraw () { + using (vkvg.Context ctx = vkvgPipeline.CreateContext()) { + ctx.Operator = vkvg.Operator.Clear; + ctx.Paint (); + ctx.Operator = vkvg.Operator.Over; + vkvgPipeline.DrawResources (ctx, (int)swapChain.Width, (int)swapChain.Height); + } + } + + +#endif + + + + Program () : base () { + vbo = new GPUBuffer (presentQueue, cmdPool, VkBufferUsageFlags.VertexBuffer, g_vertex_buffer_data); + + descriptorPool = new DescriptorPool (dev, 2, + new VkDescriptorPoolSize (VkDescriptorType.UniformBuffer, 2), + new VkDescriptorPoolSize (VkDescriptorType.CombinedImageSampler, 2) + ); + + dsLayout = new DescriptorSetLayout (dev, 0, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Vertex, VkDescriptorType.UniformBuffer), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler)); + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, samples); + + cfg.Layout = new PipelineLayout (dev, dsLayout); + cfg.RenderPass = new RenderPass (dev, swapChain.ColorFormat, dev.GetSuitableDepthFormat (), cfg.Samples); + + cfg.AddVertexBinding (0, 5 * sizeof (float)); + cfg.AddVertexAttributes (0, VkFormat.R32g32b32Sfloat, VkFormat.R32g32Sfloat); + + cfg.AddShader (VkShaderStageFlags.Vertex, "#TexturedCube.skybox.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "#TexturedCube.skybox.frag.spv"); + + pipeline = new GraphicPipeline (cfg); + + uboMats = new HostBuffer (dev, VkBufferUsageFlags.UniformBuffer, matrices); + uboMats.Map ();//permanent map + + descriptorSet = descriptorPool.Allocate (dsLayout); + DescriptorSetWrites uboUpdate = new DescriptorSetWrites (descriptorSet, dsLayout.Bindings[0]); + uboUpdate.Write (dev, uboMats.Descriptor); + + loadTexture (imgPathes[currentImgIndex]); + if (nextTexture != null) + updateTextureSet (); + + +#if WITH_VKVG + dsVkvg = descriptorPool.Allocate (pipeline.Layout.DescriptorSetLayouts[0]); + vkvgPipeline = new VkvgPipeline.VkvgPipeline (instance, dev, presentQueue, pipeline); + } + + public override void Update () { + vkvgDraw (); +#endif + } + + void buildCommandBuffers () { + for (int i = 0; i < swapChain.ImageCount; ++i) { + cmds[i]?.Free (); + cmds[i] = cmdPool.AllocateCommandBuffer (); + cmds[i].Start (); + + recordDraw (cmds[i], frameBuffers[i]); + + cmds[i].End (); + } + } + void recordDraw (CommandBuffer cmd, Framebuffer fb) { + pipeline.RenderPass.Begin (cmd, fb); + + cmd.SetViewport (fb.Width, fb.Height); + cmd.SetScissor (fb.Width, fb.Height); + cmd.BindDescriptorSet (pipeline.Layout, descriptorSet); + + pipeline.Bind (cmd); + + cmd.BindVertexBuffer (vbo, 0); + cmd.Draw (36); + +#if WITH_VKVG + cmd.BindDescriptorSet (pipeline.Layout, dsVkvg); + vkvgPipeline.RecordDraw (cmd); +#endif + + pipeline.RenderPass.End (cmd); + } + + //in the thread of the keyboard + void loadTexture (string path) { + try { + if (path.EndsWith ("ktx", StringComparison.OrdinalIgnoreCase)) + nextTexture = KTX.KTX.Load (presentQueue, cmdPool, path, + VkImageUsageFlags.Sampled, VkMemoryPropertyFlags.DeviceLocal, true); + else + nextTexture = Image.Load (dev, path); + updateViewRequested = true; + } catch (Exception ex) { + Console.WriteLine (ex); + nextTexture = null; + } + } + + //in the main vulkan thread + void updateTextureSet (){ + nextTexture.CreateView (VkImageViewType.Cube,VkImageAspectFlags.Color,6); + nextTexture.CreateSampler (); + + nextTexture.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + DescriptorSetWrites uboUpdate = new DescriptorSetWrites (descriptorSet, dsLayout.Bindings[1]); + uboUpdate.Write (dev, nextTexture.Descriptor); + + texture?.Dispose (); + texture = nextTexture; + nextTexture = null; + } + void updateMatrices () { + matrices.projection = Matrix4x4.CreatePerspectiveFieldOfView (Utils.DegreesToRadians (60f), (float)swapChain.Width / (float)swapChain.Height, 0.1f, 5.0f); + matrices.view = + Matrix4x4.CreateFromAxisAngle (Vector3.UnitZ, rotZ) * + Matrix4x4.CreateFromAxisAngle (Vector3.UnitY, rotY) * + Matrix4x4.CreateFromAxisAngle (Vector3.UnitX, rotX); + + uboMats.Update (matrices, (uint)Marshal.SizeOf ()); + } + + protected override void configureEnabledFeatures (VkPhysicalDeviceFeatures available_features, ref VkPhysicalDeviceFeatures enabled_features) { + base.configureEnabledFeatures (available_features, ref enabled_features); + enabled_features.textureCompressionBC = available_features.textureCompressionBC; + } + + public override void UpdateView () { + if (nextTexture != null) { + dev.WaitIdle (); + updateTextureSet (); + buildCommandBuffers (); + }else + updateMatrices (); + + updateViewRequested = false; + dev.WaitIdle (); + } + + protected override void onMouseMove (double xPos, double yPos) { + double diffX = lastMouseX - xPos; + double diffY = lastMouseY - yPos; + if (MouseButton[0]) { + rotY -= rotSpeed * (float)diffX; + rotX += rotSpeed * (float)diffY; + } else if (MouseButton[1]) { + zoom += zoomSpeed * (float)diffY; + } + + updateViewRequested = true; + } + + protected override void onKeyDown (Key key, int scanCode, Modifier modifiers) { + switch (key) { + case Key.Space: + currentImgIndex++; + if (currentImgIndex == imgPathes.Length) + currentImgIndex = 0; + loadTexture (imgPathes[currentImgIndex]); + break; + default: + base.onKeyDown (key, scanCode, modifiers); + break; + } + } + + protected override void OnResize () { + dev.WaitIdle (); + +#if WITH_VKVG + vkvgPipeline.Resize ((int)swapChain.Width, (int)swapChain.Height, new DescriptorSetWrites (dsVkvg, dsLayout.Bindings[1])); +#endif + + updateMatrices (); + + if (frameBuffers!=null) + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); + + frameBuffers = new Framebuffer[swapChain.ImageCount]; + + for (int i = 0; i < swapChain.ImageCount; ++i) { + frameBuffers[i] = new Framebuffer (pipeline.RenderPass, swapChain.Width, swapChain.Height, + (pipeline.Samples == VkSampleCountFlags.SampleCount1) ? new Image[] { + swapChain.images[i], + null + } : new Image[] { + null, + null, + swapChain.images[i] + }); + } + + buildCommandBuffers (); + + dev.WaitIdle (); + } + + protected override void Dispose (bool disposing) { + if (disposing) { + if (!isDisposed) { + dev.WaitIdle (); + pipeline.Dispose (); + for (int i = 0; i < swapChain.ImageCount; i++) + frameBuffers[i].Dispose (); + descriptorPool.Dispose (); + texture.Dispose (); + uboMats.Dispose (); + vbo.Dispose (); + +#if WITH_VKVG + vkvgPipeline.Dispose (); +#endif + } + } + + base.Dispose (disposing); + } + } +} diff --git a/samples/TexturedCube/shaders/FullScreenQuad.vert b/samples/TexturedCube/shaders/FullScreenQuad.vert new file mode 100644 index 0000000..826720b --- /dev/null +++ b/samples/TexturedCube/shaders/FullScreenQuad.vert @@ -0,0 +1,10 @@ +#version 450 + +layout (location = 0) out vec2 outUV; + +void main() +{ + outUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); + gl_Position = vec4(outUV * 2.0f + -1.0f, 0.0f, 1.0f); + //gl_Position = vec4(outUV -1.0f, 0.0f, 1.0f); +} diff --git a/samples/TexturedCube/shaders/main.frag b/samples/TexturedCube/shaders/main.frag new file mode 100644 index 0000000..73dfd79 --- /dev/null +++ b/samples/TexturedCube/shaders/main.frag @@ -0,0 +1,15 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (binding = 1) uniform sampler2D samplerColor; + +layout (location = 0) in vec2 inUV; + +layout (location = 0) out vec4 outFragColor; + +void main() +{ + outFragColor = texture(samplerColor, inUV); +} \ No newline at end of file diff --git a/samples/TexturedCube/shaders/main.vert b/samples/TexturedCube/shaders/main.vert new file mode 100644 index 0000000..3b395bf --- /dev/null +++ b/samples/TexturedCube/shaders/main.vert @@ -0,0 +1,28 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (location = 0) in vec3 inPos; +layout (location = 1) in vec3 inColor; + +layout (binding = 0) uniform UBO +{ + mat4 projectionMatrix; + mat4 viewMatrix; + mat4 modelMatrix; +} ubo; + +layout (location = 0) out vec3 outColor; + +out gl_PerVertex +{ + vec4 gl_Position; +}; + + +void main() +{ + outColor = inColor; + gl_Position = ubo.projectionMatrix * ubo.viewMatrix * ubo.modelMatrix * vec4(inPos.xyz, 1.0); +} diff --git a/samples/TexturedCube/shaders/simpletexture.frag b/samples/TexturedCube/shaders/simpletexture.frag new file mode 100644 index 0000000..cee63cb --- /dev/null +++ b/samples/TexturedCube/shaders/simpletexture.frag @@ -0,0 +1,15 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (set = 0, binding = 1) uniform sampler2D samplerColor; + +layout (location = 0) in vec2 inUV; + +layout (location = 0) out vec4 outFragColor; + +void main() +{ + outFragColor = texture(samplerColor, inUV); +} diff --git a/samples/TexturedCube/shaders/skybox.frag b/samples/TexturedCube/shaders/skybox.frag new file mode 100644 index 0000000..bdd272d --- /dev/null +++ b/samples/TexturedCube/shaders/skybox.frag @@ -0,0 +1,15 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (binding = 1) uniform samplerCube skybox; + +layout (location = 0) in vec3 TexCoords; + +layout (location = 0) out vec4 outFragColor; + +void main() +{ + outFragColor = textureLod(skybox, TexCoords,0); +} diff --git a/samples/TexturedCube/shaders/skybox.vert b/samples/TexturedCube/shaders/skybox.vert new file mode 100644 index 0000000..2d6a6b6 --- /dev/null +++ b/samples/TexturedCube/shaders/skybox.vert @@ -0,0 +1,26 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (location = 0) in vec3 inPos; +layout (location = 1) in vec2 inUV; + +layout (location = 0) out vec3 outUVW; + +layout (binding = 0) uniform UBO +{ + mat4 projection; + mat4 view; +} ubo; + +out gl_PerVertex +{ + vec4 gl_Position; +}; + +void main() +{ + outUVW = inPos; + gl_Position = ubo.projection * ubo.view * vec4(inPos, 1.0); +} diff --git a/samples/Triangle/Triangle.csproj b/samples/Triangle/Triangle.csproj new file mode 100644 index 0000000..dbb4bc8 --- /dev/null +++ b/samples/Triangle/Triangle.csproj @@ -0,0 +1,6 @@ + + + + false + + diff --git a/samples/Triangle/main.cs b/samples/Triangle/main.cs new file mode 100644 index 0000000..75c20ed --- /dev/null +++ b/samples/Triangle/main.cs @@ -0,0 +1,184 @@ +// Copyright (c) 2019 Jean-Philippe Bruyère +// +// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) +using System.Numerics; +using System.Runtime.InteropServices; +using CVKL; +using VK; + +namespace Triangle { + class Program : VkWindow { + static void Main (string[] args) { +#if DEBUG + Instance.VALIDATION = true; + Instance.DEBUG_UTILS = true; + Instance.RENDER_DOC_CAPTURE = false; +#endif + + using (Program vke = new Program ()) { + vke.Run (); + } + } + + float rotSpeed = 0.01f, zoomSpeed = 0.01f; + float rotX, rotY, rotZ = 0f, zoom = 1f; + + struct Matrices { + public Matrix4x4 projection; + public Matrix4x4 view; + public Matrix4x4 model; + } + struct Vertex { + Vector3 position; + Vector3 color; + + public Vertex (float x, float y, float z, float r, float g, float b) { + position = new Vector3 (x, y, z); + color = new Vector3 (r, g, b); + } + } + + Matrices matrices; + + HostBuffer ibo; + HostBuffer vbo; + HostBuffer uboMats; + + DescriptorPool descriptorPool; + DescriptorSetLayout dsLayout; + DescriptorSet descriptorSet; + + Framebuffer[] frameBuffers; + GraphicPipeline pipeline; + + Vertex[] vertices = { + new Vertex (-1.0f, -1.0f, 0.0f , 1.0f, 0.0f, 0.0f), + new Vertex ( 1.0f, -1.0f, 0.0f , 0.0f, 1.0f, 0.0f), + new Vertex ( 0.0f, 1.0f, 0.0f , 0.0f, 0.0f, 1.0f), + }; + ushort[] indices = new ushort[] { 0, 1, 2 }; + + Program () : base () { + vbo = new HostBuffer (dev, VkBufferUsageFlags.VertexBuffer, vertices); + ibo = new HostBuffer (dev, VkBufferUsageFlags.IndexBuffer, indices); + uboMats = new HostBuffer (dev, VkBufferUsageFlags.UniformBuffer, matrices); + + descriptorPool = new DescriptorPool (dev, 1, new VkDescriptorPoolSize (VkDescriptorType.UniformBuffer)); + dsLayout = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, VkDescriptorType.UniformBuffer)); + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, VkSampleCountFlags.SampleCount1); + + cfg.Layout = new PipelineLayout (dev, dsLayout); + cfg.RenderPass = new RenderPass (dev, swapChain.ColorFormat, dev.GetSuitableDepthFormat (), cfg.Samples); + cfg.AddVertexBinding (0); + cfg.AddVertexAttributes (0, VkFormat.R32g32b32Sfloat, VkFormat.R32g32b32Sfloat); + + cfg.AddShader (VkShaderStageFlags.Vertex, "#Triangle.main.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "#Triangle.main.frag.spv"); + + pipeline = new GraphicPipeline (cfg); + + //note that descriptor set is allocated after the pipeline creation that use this layout, layout is activated + //automaticaly on pipeline creation, and will be disposed automatically when no longuer in use. + descriptorSet = descriptorPool.Allocate (dsLayout); + + DescriptorSetWrites uboUpdate = new DescriptorSetWrites (descriptorSet, dsLayout); + uboUpdate.Write (dev, uboMats.Descriptor); + + uboMats.Map (); + } + + public override void UpdateView () { + matrices.projection = Matrix4x4.CreatePerspectiveFieldOfView (Utils.DegreesToRadians (45f), + (float)swapChain.Width / (float)swapChain.Height, 0.1f, 256.0f) * Camera.VKProjectionCorrection; + matrices.view = + Matrix4x4.CreateFromAxisAngle (Vector3.UnitZ, rotZ) * + Matrix4x4.CreateFromAxisAngle (Vector3.UnitY, rotY) * + Matrix4x4.CreateFromAxisAngle (Vector3.UnitX, rotX) * + Matrix4x4.CreateTranslation (0, 0, -3f * zoom); + matrices.model = Matrix4x4.Identity; + uboMats.Update (matrices, (uint)Marshal.SizeOf ()); + updateViewRequested = false; + } + + protected override void onMouseMove (double xPos, double yPos) { + double diffX = lastMouseX - xPos; + double diffY = lastMouseY - yPos; + if (MouseButton[0]) { + rotY -= rotSpeed * (float)diffX; + rotX += rotSpeed * (float)diffY; + } else if (MouseButton[1]) { + zoom += zoomSpeed * (float)diffY; + } + updateViewRequested = true; + } + void buildCommandBuffers() { + cmdPool.Reset (VkCommandPoolResetFlags.ReleaseResources); + cmds = cmdPool.AllocateCommandBuffer (swapChain.ImageCount); + + for (int i = 0; i < swapChain.ImageCount; ++i) { + Framebuffer fb = frameBuffers[i]; + cmds[i].Start (); + + pipeline.RenderPass.Begin (cmds[i], fb); + + cmds[i].SetViewport (swapChain.Width, swapChain.Height); + cmds[i].SetScissor (swapChain.Width, swapChain.Height); + + cmds[i].BindDescriptorSet (pipeline.Layout, descriptorSet); + + cmds[i].BindPipeline (pipeline); + + cmds[i].BindVertexBuffer (vbo); + cmds[i].BindIndexBuffer (ibo, VkIndexType.Uint16); + cmds[i].DrawIndexed ((uint)indices.Length); + + pipeline.RenderPass.End (cmds[i]); + + cmds[i].End (); + } + } + + protected override void OnResize () { + dev.WaitIdle (); + + if (frameBuffers != null) + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); + frameBuffers = new Framebuffer[swapChain.ImageCount]; + + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i] = new Framebuffer (pipeline.RenderPass, swapChain.Width, swapChain.Height, + (pipeline.Samples == VkSampleCountFlags.SampleCount1) ? new Image[] { + swapChain.images[i], + null + } : new Image[] { + null, + null, + swapChain.images[i] + }); + + buildCommandBuffers (); + + dev.WaitIdle (); + } + + protected override void Dispose (bool disposing) { + dev.WaitIdle (); + if (disposing) { + if (!isDisposed) { + pipeline.Dispose (); + for (int i = 0; i < swapChain.ImageCount; i++) + frameBuffers[i].Dispose (); + descriptorPool.Dispose (); + vbo.Dispose (); + ibo.Dispose (); + uboMats.Dispose (); + } + } + + base.Dispose (disposing); + } + } +} diff --git a/samples/Triangle/shaders/main.frag b/samples/Triangle/shaders/main.frag new file mode 100644 index 0000000..5ff09f5 --- /dev/null +++ b/samples/Triangle/shaders/main.frag @@ -0,0 +1,9 @@ +#include + +layout (location = 0) in vec3 inColor; +layout (location = 0) out vec4 outFragColor; + +void main() +{ + outFragColor = vec4(inColor, 1.0); +} \ No newline at end of file diff --git a/samples/Triangle/shaders/main.vert b/samples/Triangle/shaders/main.vert new file mode 100644 index 0000000..542d015 --- /dev/null +++ b/samples/Triangle/shaders/main.vert @@ -0,0 +1,30 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (location = 0) in vec3 inPos; +layout (location = 1) in vec3 inColor; + +layout (binding = 0) uniform UBO +{ + mat4 projectionMatrix; + mat4 viewMatrix; + mat4 modelMatrix; +} ubo; + +layout (location = 0) out vec3 outColor; + +out gl_PerVertex +{ + vec4 gl_Position; +}; + + +void main() +{ + outColor = inColor; + gl_Position = ubo.projectionMatrix * ubo.viewMatrix * ubo.modelMatrix * vec4(inPos.xyz, 1.0); + //gl_Position.y = -gl_Position.y; + //gl_Position.z = (gl_Position.z + gl_Position.w) / 2.0; +} diff --git a/samples/common/CrowWin.cs b/samples/common/CrowWin.cs new file mode 100644 index 0000000..4eb8730 --- /dev/null +++ b/samples/common/CrowWin.cs @@ -0,0 +1,254 @@ +using System; +using Glfw; +using VK; +using System.Threading; + +namespace Crow { + public class CrowWin : CVKL.VkWindow, IValueChange { + #region IValueChange implementation + public event EventHandler ValueChanged; + public virtual void NotifyValueChanged (string MemberName, object _value) { + if (ValueChanged != null) + ValueChanged.Invoke (this, new ValueChangeEventArgs (MemberName, _value)); + } + #endregion + + CVKL.DescriptorPool descriptorPool; + CVKL.DescriptorSetLayout descLayout; + CVKL.DescriptorSet dsCrow; + + CVKL.GraphicPipeline uiPipeline; + CVKL.Framebuffer[] uiFrameBuffers; + + protected Interface crow; + protected vkvg.Device vkvgDev; + protected CVKL.Image uiImage; + protected bool isRunning; + + protected CrowWin (string name = "CrowWin", uint _width = 1024, uint _height = 768, bool vSync = false) : + base (name, _width, _height, vSync) { + + Thread crowThread = new Thread (crow_thread_func); + crowThread.IsBackground = true; + crowThread.Start (); + + while (crow == null) + Thread.Sleep (2); + + initUISurface (); + + initUIPipeline (); + } + + protected override void render () { + int idx = swapChain.GetNextImage (); + + if (idx < 0) { + OnResize (); + return; + } + + lock (crow.RenderMutex) { + presentQueue.Submit (cmds[idx], swapChain.presentComplete, drawComplete[idx]); + presentQueue.Present (swapChain, drawComplete[idx]); + presentQueue.WaitIdle (); + } + Thread.Sleep (1); + } + + void initUIPipeline (VkSampleCountFlags samples = VkSampleCountFlags.SampleCount1) { + descriptorPool = new CVKL.DescriptorPool (dev, 1, new VkDescriptorPoolSize (VkDescriptorType.CombinedImageSampler)); + descLayout = new CVKL.DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler) + ); + + CVKL.GraphicPipelineConfig cfg = CVKL.GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, samples, false); + cfg.Layout = new CVKL.PipelineLayout (dev, descLayout); + cfg.RenderPass = new CVKL.RenderPass (dev, swapChain.ColorFormat, samples); + cfg.AddShader (VkShaderStageFlags.Vertex, "#deferred.FullScreenQuad.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "#deferred.simpletexture.frag.spv"); + + cfg.blendAttachments[0] = new VkPipelineColorBlendAttachmentState (true); + + uiPipeline = new CVKL.GraphicPipeline (cfg); + + dsCrow = descriptorPool.Allocate (descLayout); + } + void initUISurface () { + lock (crow.UpdateMutex) { + uiImage?.Dispose (); + uiImage = new CVKL.Image (dev, new VkImage ((ulong)crow.surf.VkImage.ToInt64 ()), VkFormat.B8g8r8a8Unorm, + VkImageUsageFlags.Sampled, swapChain.Width, swapChain.Height); + uiImage.SetName ("uiImage"); + uiImage.CreateView (VkImageViewType.ImageView2D, VkImageAspectFlags.Color); + uiImage.CreateSampler (VkFilter.Nearest, VkFilter.Nearest, VkSamplerMipmapMode.Nearest, VkSamplerAddressMode.ClampToBorder); + uiImage.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + } + } + + void crow_thread_func () { + vkvgDev = new vkvg.Device (instance.Handle, phy.Handle, dev.Handle, presentQueue.qFamIndex, + vkvg.SampleCount.Sample_4, presentQueue.index); + + crow = new Interface (vkvgDev, (int)swapChain.Width, (int)swapChain.Height); + + isRunning = true; + while (isRunning) { + crow.Update (); + Thread.Sleep (2); + } + + dev.WaitIdle (); + crow.Dispose (); + vkvgDev.Dispose (); + crow = null; + } + + protected void loadWindow (string path, object dataSource = null) { + try { + Widget w = crow.FindByName (path); + if (w != null) { + crow.PutOnTop (w); + return; + } + w = crow.Load (path); + w.Name = path; + w.DataSource = dataSource; + + } catch (Exception ex) { + System.Diagnostics.Debug.WriteLine (ex.ToString ()); + } + } + protected virtual void recordDraw (CVKL.CommandBuffer cmd, int imageIndex) { } + + void buildCommandBuffers () { + for (int i = 0; i < swapChain.ImageCount; ++i) { + cmds[i]?.Free (); + cmds[i] = cmdPool.AllocateAndStart (); + + CVKL.CommandBuffer cmd = cmds[i]; + + recordDraw (cmd, i); + + uiPipeline.RenderPass.Begin (cmd, uiFrameBuffers[i]); + + uiPipeline.Bind (cmd); + cmd.BindDescriptorSet (uiPipeline.Layout, dsCrow); + + uiImage.SetLayout (cmd, VkImageAspectFlags.Color, VkImageLayout.ColorAttachmentOptimal, VkImageLayout.ShaderReadOnlyOptimal, + VkPipelineStageFlags.ColorAttachmentOutput, VkPipelineStageFlags.FragmentShader); + + cmd.Draw (3, 1, 0, 0); + + uiImage.SetLayout (cmd, VkImageAspectFlags.Color, VkImageLayout.ShaderReadOnlyOptimal, VkImageLayout.ColorAttachmentOptimal, + VkPipelineStageFlags.FragmentShader, VkPipelineStageFlags.BottomOfPipe); + + uiPipeline.RenderPass.End (cmd); + + cmds[i].End (); + } + } + + ///

+ /// rebuild command buffers if needed + /// + public override void Update () { + if (rebuildBuffers) { + buildCommandBuffers (); + rebuildBuffers = false; + } + } + + protected override void OnResize () { + dev.WaitIdle (); + + crow.ProcessResize (new Rectangle (0, 0, (int)swapChain.Width, (int)swapChain.Height)); + + initUISurface (); + + CVKL.DescriptorSetWrites uboUpdate = new CVKL.DescriptorSetWrites (dsCrow, descLayout); + uboUpdate.Write (dev, uiImage.Descriptor); + + if (uiFrameBuffers != null) + for (int i = 0; i < swapChain.ImageCount; ++i) + uiFrameBuffers[i]?.Dispose (); + + uiFrameBuffers = new CVKL.Framebuffer[swapChain.ImageCount]; + + for (int i = 0; i < swapChain.ImageCount; ++i) { + uiFrameBuffers[i] = new CVKL.Framebuffer (uiPipeline.RenderPass, swapChain.Width, swapChain.Height, + (uiPipeline.Samples == VkSampleCountFlags.SampleCount1) ? new CVKL.Image[] { + swapChain.images[i], + } : new CVKL.Image[] { + null, + swapChain.images[i] + }); + uiFrameBuffers[i].SetName ("ui FB " + i); + } + + buildCommandBuffers (); + dev.WaitIdle (); + } + + #region Mouse and keyboard + protected override void onScroll (double xOffset, double yOffset) { + if (KeyModifiers.HasFlag (Modifier.Shift)) + crow.ProcessMouseWheelChanged ((float)xOffset); + else + crow.ProcessMouseWheelChanged ((float)yOffset); + } + protected override void onMouseMove (double xPos, double yPos) { + if (crow.ProcessMouseMove ((int)xPos, (int)yPos)) + return; + base.onMouseMove (xPos, yPos); + } + protected override void onMouseButtonDown (Glfw.MouseButton button) { + if (crow.ProcessMouseButtonDown ((MouseButton)button)) + return; + base.onMouseButtonDown (button); + } + protected override void onMouseButtonUp (Glfw.MouseButton button) { + if (crow.ProcessMouseButtonUp ((MouseButton)button)) + return; + base.onMouseButtonUp (button); + } + protected override void onKeyDown (Glfw.Key key, int scanCode, Modifier modifiers) { + if (crow.ProcessKeyDown ((Key)key)) + return; + base.onKeyDown (key, scanCode, modifiers); + } + protected override void onKeyUp (Glfw.Key key, int scanCode, Modifier modifiers) { + if (crow.ProcessKeyUp ((Key)key)) + return; + } + protected override void onChar (CodePoint cp) { + if (crow.ProcessKeyPress (cp.ToChar ())) + return; + } + #endregion + + #region dispose + protected override void Dispose (bool disposing) { + if (disposing) { + if (!isDisposed) { + dev.WaitIdle (); + isRunning = false; + + for (int i = 0; i < swapChain.ImageCount; ++i) + uiFrameBuffers[i]?.Dispose (); + + uiPipeline.Dispose (); + descLayout.Dispose (); + descriptorPool.Dispose (); + + uiImage?.Dispose (); + while (crow != null) + Thread.Sleep (1); + } + } + + base.Dispose (disposing); + } + #endregion + } +} diff --git a/samples/common/shaders/preamble.inc b/samples/common/shaders/preamble.inc new file mode 100644 index 0000000..da434f8 --- /dev/null +++ b/samples/common/shaders/preamble.inc @@ -0,0 +1,4 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable diff --git a/samples/compute/compute.csproj b/samples/compute/compute.csproj new file mode 100644 index 0000000..e1734ff --- /dev/null +++ b/samples/compute/compute.csproj @@ -0,0 +1,10 @@ + + + + false + + + + + + \ No newline at end of file diff --git a/samples/compute/delaunay.cs b/samples/compute/delaunay.cs new file mode 100644 index 0000000..a01cd85 --- /dev/null +++ b/samples/compute/delaunay.cs @@ -0,0 +1,329 @@ +using System; +using Glfw; +using VK; +using CVKL; + +namespace delaunay { + class Program : VkWindow { + static void Main (string[] args) { +#if DEBUG + Instance.VALIDATION = true; + Instance.DEBUG_UTILS = true; + Instance.RENDER_DOC_CAPTURE = false; +#endif + using (Program vke = new Program ()) { + vke.Run (); + } + } + + Framebuffer[] frameBuffers; + GraphicPipeline grPipeline; + + Image imgResult; + + Queue computeQ, transferQ; + + GPUBuffer inBuff, outBuff; + HostBuffer stagingDataBuff; + DescriptorPool dsPool; + DescriptorSetLayout dslCompute, dslImage; + DescriptorSet dsetPing, dsetPong, dsImage; + + ComputePipeline plCompute, plNormalize; + + DebugReport dbgReport; + + const uint imgDim = 256; + uint zoom = 2; + int invocationCount = 8; + + uint data_size => imgDim * imgDim * 4; + + float[] datas; + + uint seedCount; + + void addSeed (uint x, uint y) { + uint ptr = (y * imgDim + x) * 4; + datas[ptr] = ++seedCount;//seedId + datas[ptr + 1] = x; + datas[ptr + 2] = y; + datas[ptr + 3] = 1; + + } + + + public Program () : base () { + if (Instance.DEBUG_UTILS) + dbgReport = new DebugReport (instance, + VkDebugReportFlagsEXT.ErrorEXT + | VkDebugReportFlagsEXT.DebugEXT + | VkDebugReportFlagsEXT.WarningEXT + | VkDebugReportFlagsEXT.PerformanceWarningEXT + + ); + imgResult = new Image (dev, VkFormat.R32g32b32a32Sfloat, VkImageUsageFlags.TransferDst | VkImageUsageFlags.Sampled, VkMemoryPropertyFlags.DeviceLocal, + imgDim, imgDim); + imgResult.CreateView (); + imgResult.CreateSampler (VkFilter.Nearest, VkFilter.Nearest, VkSamplerMipmapMode.Nearest, VkSamplerAddressMode.ClampToBorder); + imgResult.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + + datas = new float[data_size]; + + addSeed (imgDim / 2 - 1, imgDim / 2 - 1); + + + stagingDataBuff = new HostBuffer (dev, VkBufferUsageFlags.TransferSrc, datas); + stagingDataBuff.Map (); + + inBuff = new GPUBuffer (dev, VkBufferUsageFlags.StorageBuffer | VkBufferUsageFlags.TransferSrc | VkBufferUsageFlags.TransferDst, (int)data_size); + outBuff = new GPUBuffer (dev, VkBufferUsageFlags.StorageBuffer | VkBufferUsageFlags.TransferSrc, (int)data_size); + + dsPool = new DescriptorPool (dev, 3, + new VkDescriptorPoolSize (VkDescriptorType.CombinedImageSampler), + new VkDescriptorPoolSize (VkDescriptorType.StorageBuffer, 4)); + dslImage = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler) + ); + dslCompute = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Compute, VkDescriptorType.StorageBuffer), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Compute, VkDescriptorType.StorageBuffer) + ); + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, VkSampleCountFlags.SampleCount1); + + cfg.Layout = new PipelineLayout (dev, dslImage); + cfg.RenderPass = new RenderPass (dev, swapChain.ColorFormat, dev.GetSuitableDepthFormat (), VkSampleCountFlags.SampleCount1); + cfg.RenderPass.ClearValues[0] = new VkClearValue { color = new VkClearColorValue (0.0f, 0.1f, 0.0f) }; + + cfg.ResetShadersAndVerticesInfos (); + cfg.AddShader (VkShaderStageFlags.Vertex, "#compute.FullScreenQuad.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "#compute.simpletexture.frag.spv"); + + cfg.blendAttachments[0] = new VkPipelineColorBlendAttachmentState (true); + + grPipeline = new GraphicPipeline (cfg); + + plCompute = new ComputePipeline ( + new PipelineLayout (dev, new VkPushConstantRange (VkShaderStageFlags.Compute, 2 * sizeof (int)), dslCompute), + "#compute.computeTest.comp.spv"); + plNormalize = new ComputePipeline ( + plCompute.Layout, + "#compute.normalize.comp.spv"); + + dsImage = dsPool.Allocate (dslImage); + dsetPing = dsPool.Allocate (dslCompute); + dsetPong = dsPool.Allocate (dslCompute); + + DescriptorSetWrites dsUpdate = new DescriptorSetWrites (dsetPing, dslCompute); + dsUpdate.Write (dev, inBuff.Descriptor, outBuff.Descriptor); + dsUpdate.Write (dev, dsetPong, outBuff.Descriptor, inBuff.Descriptor); + dsUpdate = new DescriptorSetWrites (dsImage, dslImage); + dsUpdate.Write (dev, imgResult.Descriptor); + + UpdateFrequency = 5; + } + + protected override void createQueues () { + computeQ = new Queue (dev, VkQueueFlags.Compute); + transferQ = new Queue (dev, VkQueueFlags.Transfer); + + base.createQueues (); + } + + protected override void OnResize () { + + if (frameBuffers != null) + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); + frameBuffers = new Framebuffer[swapChain.ImageCount]; + + for (int i = 0; i < swapChain.ImageCount; ++i) { + frameBuffers[i] = new Framebuffer (grPipeline.RenderPass, swapChain.Width, swapChain.Height, + (grPipeline.Samples == VkSampleCountFlags.SampleCount1) ? new Image[] { + swapChain.images[i], + null + } : new Image[] { + null, + null, + swapChain.images[i] + }); + + cmds[i] = cmdPool.AllocateCommandBuffer (); + cmds[i].Start (); + + imgResult.SetLayout (cmds[i], VkImageAspectFlags.Color, + VkImageLayout.Undefined, VkImageLayout.ShaderReadOnlyOptimal, + VkPipelineStageFlags.AllCommands, VkPipelineStageFlags.FragmentShader); + + grPipeline.RenderPass.Begin (cmds[i], frameBuffers[i]); + + int xPad = (int)swapChain.Width / 2 - (int)imgDim * (int)zoom / 2; + int yPad = (int)swapChain.Height / 2- (int)imgDim * (int)zoom / 2; + + cmds[i].SetViewport (imgDim * zoom, imgDim * zoom, xPad, yPad); + cmds[i].SetScissor (imgDim * zoom, imgDim * zoom, Math.Max (0, xPad), Math.Max (0, yPad)); + + cmds[i].BindDescriptorSet (grPipeline.Layout, dsImage); + cmds[i].BindPipeline (grPipeline); + cmds[i].Draw (3, 1, 0, 0); + + grPipeline.RenderPass.End (cmds[i]); + + cmds[i].End (); + } + } + bool pong; + + public override void Update () { + initGpuBuffers (); + + using (CommandPool cmdPoolCompute = new CommandPool (dev, computeQ.qFamIndex)) { + + CommandBuffer cmd = cmdPoolCompute.AllocateAndStart (VkCommandBufferUsageFlags.OneTimeSubmit); + + pong = false; + uint stepSize = imgDim / 2; + + plCompute.Bind (cmd); + cmd.PushConstant (plCompute.Layout, VkShaderStageFlags.Compute, imgDim, sizeof(int)); + + int pass = 0; + while (stepSize > 0 && pass < invocationCount) { + cmd.PushConstant (plCompute.Layout, VkShaderStageFlags.Compute, stepSize); + + if (pong) + plCompute.BindDescriptorSet (cmd, dsetPong); + else + plCompute.BindDescriptorSet (cmd, dsetPing); + + cmd.Dispatch (imgDim, imgDim); + + VkMemoryBarrier memBar = VkMemoryBarrier.New (); + memBar.srcAccessMask = VkAccessFlags.ShaderWrite; + memBar.dstAccessMask = VkAccessFlags.ShaderRead; + Vk.vkCmdPipelineBarrier (cmd.Handle, VkPipelineStageFlags.ComputeShader, VkPipelineStageFlags.ComputeShader, VkDependencyFlags.ByRegion, + 1, ref memBar, 0, IntPtr.Zero, 0, IntPtr.Zero); + + pong = !pong; + stepSize /= 2; + pass++; + } + + plNormalize.Bind (cmd); + if (pong) + plNormalize.BindDescriptorSet (cmd, dsetPong); + else + plNormalize.BindDescriptorSet (cmd, dsetPing); + cmd.Dispatch (imgDim, imgDim); + pong = !pong; + + cmd.End (); + + computeQ.Submit (cmd); + computeQ.WaitIdle (); + } + + printResults (); + } + + protected override void onMouseButtonDown (MouseButton button) { + int xPad = (int)swapChain.Width / 2 - (int)imgDim * (int)zoom / 2; + int yPad = (int)swapChain.Height / 2 - (int)imgDim * (int)zoom / 2; + + int localX = (int)((lastMouseX - xPad) / zoom); + int localY = (int)((lastMouseY - yPad) / zoom); + + if (localX < 0 || localY < 0 || localX >= imgDim || localY >= imgDim) + base.onMouseButtonDown (button); + else { + addSeed ((uint)localX, (uint)localY); + stagingDataBuff.Update (datas); + } + } + protected override void onKeyDown (Key key, int scanCode, Modifier modifiers) { + switch (key) { + case Key.Delete: + datas = new float[data_size]; + stagingDataBuff.Update (datas); + seedCount = 0; + break; + case Key.KeypadAdd: + invocationCount++; + break; + case Key.KeypadSubtract: + if (invocationCount>0) + invocationCount--; + break; + default: + base.onKeyDown (key, scanCode, modifiers); + break; + } + Console.WriteLine ($"break after {invocationCount} step"); + } + + void printResults () { + dev.WaitIdle (); + using (CommandPool cmdPoolTransfer = new CommandPool (dev, transferQ.qFamIndex)) { + + CommandBuffer cmd = cmdPoolTransfer.AllocateAndStart (VkCommandBufferUsageFlags.OneTimeSubmit); + + imgResult.SetLayout (cmd, VkImageAspectFlags.Color, + VkImageLayout.ShaderReadOnlyOptimal, VkImageLayout.TransferDstOptimal, + VkPipelineStageFlags.FragmentShader, VkPipelineStageFlags.Transfer); + + if (pong) + outBuff.CopyTo (cmd, imgResult, VkImageLayout.ShaderReadOnlyOptimal); + else + inBuff.CopyTo (cmd, imgResult, VkImageLayout.ShaderReadOnlyOptimal); + + cmd.End (); + + transferQ.Submit (cmd); + transferQ.WaitIdle (); + } + } + + void initGpuBuffers () { + using (CommandPool staggingCmdPool = new CommandPool (dev, transferQ.qFamIndex)) { + CommandBuffer cmd = staggingCmdPool.AllocateAndStart (VkCommandBufferUsageFlags.OneTimeSubmit); + + stagingDataBuff.CopyTo (cmd, inBuff); + + transferQ.EndSubmitAndWait (cmd); + } + } + + protected override void Dispose (bool disposing) { + if (disposing) { + if (!isDisposed) { + dev.WaitIdle (); + + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); + + grPipeline.Dispose (); + plCompute.Dispose (); + plNormalize.Dispose (); + + dslCompute.Dispose (); + dslImage.Dispose (); + + dsPool.Dispose (); + + inBuff.Dispose (); + outBuff.Dispose (); + stagingDataBuff.Dispose (); + + imgResult.Dispose (); + + dbgReport?.Dispose (); + } + } + + base.Dispose (disposing); + } + + + } +} diff --git a/samples/compute/main.cs b/samples/compute/main.cs new file mode 100644 index 0000000..02f7069 --- /dev/null +++ b/samples/compute/main.cs @@ -0,0 +1,133 @@ +using System; +using System.Runtime.InteropServices; +using CVKL; +using VK; +using System.Linq; + +namespace SimpleCompute { + class Program : IDisposable { + VkPhysicalDeviceFeatures enabledFeatures = default (VkPhysicalDeviceFeatures); + string[] enabledExtensions = { Ext.D.VK_KHR_swapchain }; + + Instance instance; + PhysicalDevice phy; + Device dev; + Queue computeQ; + + HostBuffer inBuff, outBuff; + DescriptorPool dsPool; + DescriptorSetLayout dsLayout; + DescriptorSet dset; + + ComputePipeline plCompute; + + DebugReport dbgReport; + + const uint data_size = 256; + int[] datas; + + public Program () { + instance = new Instance (); + +#if DEBUG + dbgReport = new DebugReport (instance, + VkDebugReportFlagsEXT.ErrorEXT + | VkDebugReportFlagsEXT.DebugEXT + | VkDebugReportFlagsEXT.WarningEXT + | VkDebugReportFlagsEXT.PerformanceWarningEXT + + ); +#endif + + phy = instance.GetAvailablePhysicalDevice ().FirstOrDefault (); + dev = new Device (phy); + computeQ = new Queue (dev, VkQueueFlags.Compute); + dev.Activate (enabledFeatures, enabledExtensions); + + datas = new int[data_size]; + Random rnd = new Random (); + for (uint i = 0; i < data_size; i++) { + datas[i] = rnd.Next (); + } + + inBuff = new HostBuffer (dev, VkBufferUsageFlags.StorageBuffer, datas); + outBuff = new HostBuffer (dev, VkBufferUsageFlags.StorageBuffer, data_size); + + dsPool = new DescriptorPool (dev, 1, new VkDescriptorPoolSize (VkDescriptorType.StorageBuffer, 2)); + dsLayout = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Compute, VkDescriptorType.StorageBuffer), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Compute, VkDescriptorType.StorageBuffer) + ); + + plCompute = new ComputePipeline (new PipelineLayout (dev, dsLayout), "shaders/compute.comp.spv" ); + + dset = dsPool.Allocate (dsLayout); + DescriptorSetWrites dsUpdate = new DescriptorSetWrites (dset, dsLayout); + dsUpdate.Write (dev, inBuff.Descriptor, outBuff.Descriptor); + } + + + + public void Run () { + using (CommandPool cmdPool = new CommandPool (dev, computeQ.qFamIndex)) { + + CommandBuffer cmd = cmdPool.AllocateAndStart (VkCommandBufferUsageFlags.OneTimeSubmit); + + plCompute.Bind (cmd); + plCompute.BindDescriptorSet (cmd, dset); + + cmd.Dispatch (data_size * sizeof (int)); + + cmd.End (); + + computeQ.Submit (cmd); + computeQ.WaitIdle (); + } + + printResults (); + } + + void printResults () { + int[] results = new int[data_size]; + + outBuff.Map (); + Marshal.Copy (outBuff.MappedData, results, 0, results.Length); + + Console.ForegroundColor = ConsoleColor.DarkBlue; + Console.Write ("IN :"); + for (int i = 0; i < data_size; i++) { + Console.Write ($" {datas[i]} "); + } + Console.WriteLine (); + Console.Write ("OUT:"); + for (int i = 0; i < data_size; i++) { + Console.Write ($" {results[i]} "); + } + Console.WriteLine (); + outBuff.Unmap (); + } + + public void Dispose () { + dev.WaitIdle (); + + plCompute.Dispose (); + dsLayout.Dispose (); + dsPool.Dispose (); + + inBuff.Dispose (); + outBuff.Dispose (); + + dev.Dispose (); + +#if DEBUG + dbgReport.Dispose (); +#endif + instance.Dispose (); + } + + static void Main (string[] args) { + using (Program vke = new Program ()) + vke.Run (); + } + } +} diff --git a/samples/compute/shaders/FullScreenQuad.vert b/samples/compute/shaders/FullScreenQuad.vert new file mode 100644 index 0000000..826720b --- /dev/null +++ b/samples/compute/shaders/FullScreenQuad.vert @@ -0,0 +1,10 @@ +#version 450 + +layout (location = 0) out vec2 outUV; + +void main() +{ + outUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); + gl_Position = vec4(outUV * 2.0f + -1.0f, 0.0f, 1.0f); + //gl_Position = vec4(outUV -1.0f, 0.0f, 1.0f); +} diff --git a/samples/compute/shaders/compute.comp b/samples/compute/shaders/compute.comp new file mode 100644 index 0000000..10cfddd --- /dev/null +++ b/samples/compute/shaders/compute.comp @@ -0,0 +1,23 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable + +layout(binding = 0) buffer buffIn { + int dataIn[]; +}; + +layout(binding = 1) buffer buffOut { + int dataOut[]; +}; + +//layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in; + +void main() +{ + uint i = gl_GlobalInvocationID.x; + int d = dataIn[i]; + + for (int j=0; j<8; j++) + d += 1; + dataOut[i] = d; +} diff --git a/samples/compute/shaders/computeTest.comp b/samples/compute/shaders/computeTest.comp new file mode 100644 index 0000000..f24bee3 --- /dev/null +++ b/samples/compute/shaders/computeTest.comp @@ -0,0 +1,80 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable + +layout(binding = 0) buffer buffIn { + vec4 dataIn[]; +}; + +layout(binding = 1) buffer buffOut { + vec4 dataOut[]; +}; + +layout(set = 1, binding = 0) buffer VBO { + vec2 vertices[]; +}; + +layout(set = 1, binding = 1) buffer IBO { + uint indices[]; +}; + + +layout(push_constant) uniform PushConsts { + int iStepSize; + int imgDim; + int pointCount; +}; + +const ivec2 dirs[] = ivec2[8] ( + ivec2( 1, 0), + ivec2( 1, 1), + ivec2( 0, 1), + ivec2(-1, 1), + ivec2(-1, 0), + ivec2(-1,-1), + ivec2( 0,-1), + ivec2( 1,-1) +); + +layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in; + + +vec4 getPixel (ivec2 uv) { + return dataIn[uv.y * imgDim + uv.x]; +} +void setPixel (ivec2 uv, vec4 pix) { + dataOut[uv.y * imgDim + uv.x] = pix; +} + +void main() +{ + ivec2 uv = ivec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y); + vec4 d = getPixel (uv); + + ivec2 thisSeedPos; + if (d.r > 0.0) + thisSeedPos = ivec2(int(d.g), int(d.b)); + else//no seed in current pixel + thisSeedPos = ivec2(-1); + + for (int j=0; j<8; j++){ + ivec2 otherUV = uv + iStepSize * dirs[j]; + if (otherUV.x < 0 || otherUV.y < 0 || otherUV.x >= imgDim || otherUV.y >= imgDim) + continue; + + vec4 other = getPixel (otherUV); + + if (other.r > 0.0) {//seed in other + ivec2 otherSeedPos = ivec2(int(other.g), int(other.b)); + if (thisSeedPos.x < 0) {//replace current + d = other; + thisSeedPos = otherSeedPos; + }else if (distance (uv, thisSeedPos) > distance (uv, otherSeedPos)) { + d = other; + thisSeedPos = otherSeedPos; + } + } + } + + setPixel (uv, d); +} diff --git a/samples/compute/shaders/delaunay.comp b/samples/compute/shaders/delaunay.comp new file mode 100644 index 0000000..410dcee --- /dev/null +++ b/samples/compute/shaders/delaunay.comp @@ -0,0 +1,70 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable + +layout(binding = 0) buffer buffIn { + vec4 dataIn[]; +}; + +layout(binding = 1) buffer buffOut { + vec4 dataOut[]; +}; + +layout(push_constant) uniform PushConsts { + int iStepSize; + int imgDim; +}; + +const ivec2 dirs[] = ivec2[8] ( + ivec2( 1, 0), + ivec2( 1, 1), + ivec2( 0, 1), + ivec2(-1, 1), + ivec2(-1, 0), + ivec2(-1,-1), + ivec2( 0,-1), + ivec2( 1,-1) +); + +layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in; + + +vec4 getPixel (ivec2 uv) { + return dataIn[uv.y * imgDim + uv.x]; +} +void setPixel (ivec2 uv, vec4 pix) { + dataOut[uv.y * imgDim + uv.x] = pix; +} + +void main() +{ + ivec2 uv = ivec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y); + vec4 d = getPixel (uv); + + ivec2 thisSeedPos; + if (d.r > 0.0) + thisSeedPos = ivec2(int(d.g), int(d.b)); + else//no seed in current pixel + thisSeedPos = ivec2(-1); + + for (int j=0; j<8; j++){ + ivec2 otherUV = uv + iStepSize * dirs[j]; + if (otherUV.x < 0 || otherUV.y < 0 || otherUV.x >= imgDim || otherUV.y >= imgDim) + continue; + + vec4 other = getPixel (otherUV); + + if (other.r > 0.0) {//seed in other + ivec2 otherSeedPos = ivec2(int(other.g), int(other.b)); + if (thisSeedPos.x < 0) {//replace current + d = other; + thisSeedPos = otherSeedPos; + }else if (distance (uv, thisSeedPos) > distance (uv, otherSeedPos)) { + d = other; + thisSeedPos = otherSeedPos; + } + } + } + + setPixel (uv, d); +} diff --git a/samples/compute/shaders/init.comp b/samples/compute/shaders/init.comp new file mode 100644 index 0000000..fc615ee --- /dev/null +++ b/samples/compute/shaders/init.comp @@ -0,0 +1,36 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable + +layout(binding = 0) buffer buffIn { + vec4 dataIn[]; +}; + +layout(binding = 1) buffer buffOut { + vec4 dataOut[]; +}; + +layout(set = 1, binding = 0) buffer VBO { + vec2 vertices[]; +}; + +layout(set = 1, binding = 1) buffer IBO { + uint indices[]; +}; + +layout(push_constant) uniform PushConsts { + int iStepSize; + int imgDim; + int pointCount; +}; + + +layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in; + +void main() +{ + vec2 v = vertices[gl_GlobalInvocationID.x]; + ivec2 uv = ivec2(int(v.x), int(v.y)); + dataIn[uv.y * imgDim + uv.x] = vec4 (gl_GlobalInvocationID.x + 1, uv.x, uv.y, 1.0); +} + diff --git a/samples/compute/shaders/mandelbrot.comp b/samples/compute/shaders/mandelbrot.comp new file mode 100644 index 0000000..a03f4a0 --- /dev/null +++ b/samples/compute/shaders/mandelbrot.comp @@ -0,0 +1,56 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable + +#define WIDTH 3200 +#define HEIGHT 2400 +#define WORKGROUP_SIZE 32 +layout (local_size_x = WORKGROUP_SIZE, local_size_y = WORKGROUP_SIZE, local_size_z = 1 ) in; + +struct Pixel{ + vec4 value; +}; + +layout(std140, binding = 0) buffer buf +{ + Pixel imageData[]; +}; + +void main() { + + /* + In order to fit the work into workgroups, some unnecessary threads are launched. + We terminate those threads here. + */ + if(gl_GlobalInvocationID.x >= WIDTH || gl_GlobalInvocationID.y >= HEIGHT) + return; + + float x = float(gl_GlobalInvocationID.x) / float(WIDTH); + float y = float(gl_GlobalInvocationID.y) / float(HEIGHT); + + /* + What follows is code for rendering the mandelbrot set. + */ + vec2 uv = vec2(x,y); + float n = 0.0; + vec2 c = vec2(-.445, 0.0) + (uv - 0.5)*(2.0+ 1.7*0.2 ), + z = vec2(0.0); + const int M =128; + for (int i = 0; i 2) break; + n++; + } + + // we use a simple cosine palette to determine color: + // http://iquilezles.org/www/articles/palettes/palettes.htm + float t = float(n) / float(M); + vec3 d = vec3(0.3, 0.3 ,0.5); + vec3 e = vec3(-0.2, -0.3 ,-0.5); + vec3 f = vec3(2.1, 2.0, 3.0); + vec3 g = vec3(0.0, 0.1, 0.0); + vec4 color = vec4( d + e*cos( 6.28318*(f*t+g) ) ,1.0); + + // store the rendered mandelbrot set into a storage buffer: + imageData[WIDTH * gl_GlobalInvocationID.y + gl_GlobalInvocationID.x].value = color; +} \ No newline at end of file diff --git a/samples/compute/shaders/normalize.comp b/samples/compute/shaders/normalize.comp new file mode 100644 index 0000000..2bc6aa6 --- /dev/null +++ b/samples/compute/shaders/normalize.comp @@ -0,0 +1,73 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable + +layout(binding = 0) buffer buffIn { + vec4 dataIn[]; +}; + +layout(binding = 1) buffer buffOut { + vec4 dataOut[]; +}; + +layout(set = 1, binding = 0) buffer VBO { + vec2 vertices[]; +}; + +layout(set = 1, binding = 1) buffer IBO { + uint indices[]; +}; + +layout(push_constant) uniform PushConsts { + int iStepSize; + int imgDim; + int pointCount; +}; + +layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in; + +vec4 getPixel (ivec2 uv) { + return dataIn[uv.y * imgDim + uv.x]; +} +void setPixel (ivec2 uv, vec4 pix) { + dataOut[uv.y * imgDim + uv.x] = pix; +} + +const vec4[] colors = vec4[]( + vec4(1,0,0,1), + vec4(0,1,0,1), + vec4(0,0,1,1), + vec4(1,0,1,1), + vec4(0,1,1,1), + vec4(1,1,0,1), + vec4(0.5,0,0,1), + vec4(0,0.5,0,1), + vec4(0,0,0.5,1), + vec4(0.5,0,0.5,1), + vec4(0,0.5,0.5,1), + vec4(0.5,0.5,0,1), + vec4(0,0.5,0.5,1), + vec4(0.1,0.9,0.2,1), + vec4(0.3,0.7,0.4,1), + vec4(0.5,0.5,0.6,1), + vec4(0.7,0.3,0.8,1), + vec4(0.9,0.1,0.0,1) + ); + +void main() +{ + ivec2 uv = ivec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y); + vec4 d = getPixel (uv); + + if (d.r > 0.0) { + if (int(d.g) == uv.x && int(d.b) == uv.y) + d.a = 1.0; + else + d.a = 0.4; + + d.rgb = colors [int(d.r)].rgb; + } + + setPixel (uv, d); +} + diff --git a/samples/compute/shaders/simpletexture.frag b/samples/compute/shaders/simpletexture.frag new file mode 100644 index 0000000..a081876 --- /dev/null +++ b/samples/compute/shaders/simpletexture.frag @@ -0,0 +1,15 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (set = 0, binding = 0) uniform sampler2D samplerColor; + +layout (location = 0) in vec2 inUV; + +layout (location = 0) out vec4 outFragColor; + +void main() +{ + outFragColor = texture(samplerColor, inUV); +} diff --git a/samples/compute/shaders/test.comp b/samples/compute/shaders/test.comp new file mode 100644 index 0000000..8ab1ec7 --- /dev/null +++ b/samples/compute/shaders/test.comp @@ -0,0 +1,21 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable + +layout(binding = 0) buffer readonly buff { + vec4 data[]; +}; +layout(binding = 1) buffer writeonly buffOut { + vec4 dataOut[]; +}; +layout(push_constant) uniform PushConsts { + int iStepSize; + int imgDim; +}; + + +void main() +{ + ivec2 uv = ivec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y); + +} diff --git a/samples/compute/shaders/triangle2.frag b/samples/compute/shaders/triangle2.frag new file mode 100644 index 0000000..3f905f7 --- /dev/null +++ b/samples/compute/shaders/triangle2.frag @@ -0,0 +1,13 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +//layout (location = 0) in vec3 inColor; + +layout (location = 0) out vec4 outFragColor; + +void main() +{ + outFragColor = vec4(1.0, 1.0, 1.0, 1.0); +} \ No newline at end of file diff --git a/samples/compute/shaders/triangle2.vert b/samples/compute/shaders/triangle2.vert new file mode 100644 index 0000000..2922694 --- /dev/null +++ b/samples/compute/shaders/triangle2.vert @@ -0,0 +1,24 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (location = 0) in vec2 inPos; + +layout(push_constant) uniform PushConsts { + int imgDim; + int xPad; + int yPad; + int zoom; +}; + +out gl_PerVertex +{ + vec4 gl_Position; +}; + + +void main() +{ + gl_Position = vec4(inPos.xy * vec2(2) / vec2(imgDim) - vec2(1), 0.0, 1.0); +} diff --git a/samples/compute/test.cs b/samples/compute/test.cs new file mode 100644 index 0000000..80e66d4 --- /dev/null +++ b/samples/compute/test.cs @@ -0,0 +1,133 @@ +using System; +using System.Runtime.InteropServices; +using CVKL; +using VK; +using System.Linq; + +namespace test { + class Program : IDisposable { + VkPhysicalDeviceFeatures enabledFeatures = default (VkPhysicalDeviceFeatures); + string[] enabledExtensions = { Ext.D.VK_KHR_swapchain }; + + static void Main (string[] args) { + Instance.VALIDATION = true; + //Instance.DEBUG_UTILS = true; + + using (Program vke = new Program ()) + vke.Run (); + } + + Instance instance; + PhysicalDevice phy; + Device dev; + Queue computeQ; + + HostBuffer inBuff, outBuff; + DescriptorPool dsPool; + DescriptorSetLayout dsLayoutCompute; + DescriptorSet dsetPing, dsetPong; + + ComputePipeline plCompute; + + DebugReport dbgReport; + + const uint imgDim = 256; + + uint data_size => imgDim * imgDim * 4; + + float[] datas; + + public Program () { + instance = new Instance (); + +#if DEBUG + /*dbgReport = new DebugReport (instance, + VkDebugReportFlagsEXT.ErrorEXT + | VkDebugReportFlagsEXT.DebugEXT + | VkDebugReportFlagsEXT.WarningEXT + | VkDebugReportFlagsEXT.PerformanceWarningEXT + + );*/ +#endif + + phy = instance.GetAvailablePhysicalDevice ().FirstOrDefault (); + dev = new Device (phy); + computeQ = new Queue (dev, VkQueueFlags.Compute); + dev.Activate (enabledFeatures, enabledExtensions); + + datas = new float[data_size]; + Random rnd = new Random (); + for (uint i = 0; i < data_size; i++) { + datas[i] = (float)rnd.NextDouble (); + } + + inBuff = new HostBuffer (dev, VkBufferUsageFlags.StorageBuffer, datas); + outBuff = new HostBuffer (dev, VkBufferUsageFlags.StorageBuffer, data_size); + + dsPool = new DescriptorPool (dev, 2, new VkDescriptorPoolSize (VkDescriptorType.StorageBuffer, 4)); + dsLayoutCompute = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Compute, VkDescriptorType.StorageBuffer), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Compute, VkDescriptorType.StorageBuffer) + ); + + plCompute = new ComputePipeline ( + new PipelineLayout (dev, new VkPushConstantRange (VkShaderStageFlags.Compute, sizeof (int)), dsLayoutCompute), + "#compute.computeTest.comp.spv" ); + + dsetPing = dsPool.Allocate (dsLayoutCompute); + dsetPong = dsPool.Allocate (dsLayoutCompute); + DescriptorSetWrites dsUpdate = new DescriptorSetWrites (dsetPing, dsLayoutCompute); + dsUpdate.Write (dev, inBuff.Descriptor, outBuff.Descriptor); + + dsUpdate.Write (dev, dsetPong, outBuff.Descriptor, inBuff.Descriptor); + } + + + + public void Run () { + using (CommandPool cmdPool = new CommandPool (dev, computeQ.qFamIndex)) { + + CommandBuffer cmd = cmdPool.AllocateAndStart (VkCommandBufferUsageFlags.OneTimeSubmit); + + bool pong = false; + + plCompute.Bind (cmd); + cmd.PushConstant (plCompute.Layout, VkShaderStageFlags.Compute, imgDim); + + for (int i = 0; i < 4; i++) { + if (pong) + plCompute.BindDescriptorSet (cmd, dsetPong); + else + plCompute.BindDescriptorSet (cmd, dsetPing); + + cmd.Dispatch (imgDim, imgDim); + } + + cmd.End (); + + computeQ.Submit (cmd); + computeQ.WaitIdle (); + } + + } + + + public void Dispose () { + dev.WaitIdle (); + + plCompute.Dispose (); + dsLayoutCompute.Dispose (); + dsPool.Dispose (); + + inBuff.Dispose (); + outBuff.Dispose (); + + dev.Dispose (); + +#if DEBUG + dbgReport?.Dispose (); +#endif + instance.Dispose (); + } + } +} diff --git a/samples/compute/test2.cs b/samples/compute/test2.cs new file mode 100644 index 0000000..689e7e8 --- /dev/null +++ b/samples/compute/test2.cs @@ -0,0 +1,389 @@ +using System; +using System.Numerics; +using System.Runtime.InteropServices; +using Glfw; +using VK; +using CVKL; + +namespace triangulation { + class Program : VkWindow { + static void Main (string[] args) { + using (Program vke = new Program ()) { + vke.Run (); + } + } + + Framebuffer[] frameBuffers; + GraphicPipeline grPipeline, trianglesPipeline; + + Image imgResult; + + Queue computeQ, transferQ; + + GPUBuffer inBuff, outBuff; + HostBuffer staggingVBO; + DescriptorPool dsPool; + DescriptorSetLayout dslCompute, dslImage, dslVAO; + DescriptorSet dsPing, dsPong, dsImage, dsVAO; + + ComputePipeline plCompute, plNormalize, plInit; + + DebugReport dbgReport; + + + GPUBuffer vbo; + GPUBuffer ibo; + + uint zoom = 2; + + const int MAX_VERTICES = 128; + const uint IMG_DIM = 256; + + int invocationCount = 8; + + uint data_size => IMG_DIM * IMG_DIM * 4; + + Vector2[] points = new Vector2[MAX_VERTICES]; + uint pointCount; + bool clear = true;//if true, inBuff will be fill with zeros + + bool pong;//ping-pong between buffers + + void addPoint (uint x, uint y) { + points[pointCount] = new Vector2 (x, y); + pointCount++; + staggingVBO.Update (points, pointCount * (ulong)Marshal.SizeOf ()); + + + } + void clearPoints () { + pointCount = 0; + clear = true; + } + + public Program () : base () { +#if DEBUG + dbgReport = new DebugReport (instance, + VkDebugReportFlagsEXT.ErrorEXT + | VkDebugReportFlagsEXT.DebugEXT + | VkDebugReportFlagsEXT.WarningEXT + | VkDebugReportFlagsEXT.PerformanceWarningEXT + + ); +#endif + imgResult = new Image (dev, VkFormat.R32g32b32a32Sfloat, VkImageUsageFlags.TransferDst | VkImageUsageFlags.Sampled, VkMemoryPropertyFlags.DeviceLocal, + IMG_DIM, IMG_DIM); + imgResult.CreateView (); + imgResult.CreateSampler (VkFilter.Nearest, VkFilter.Nearest, VkSamplerMipmapMode.Nearest, VkSamplerAddressMode.ClampToBorder); + imgResult.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + + + staggingVBO = new HostBuffer (dev, VkBufferUsageFlags.TransferSrc, MAX_VERTICES); + staggingVBO.Map (); + + vbo = new GPUBuffer (dev, VkBufferUsageFlags.VertexBuffer | VkBufferUsageFlags.StorageBuffer | VkBufferUsageFlags.TransferDst, MAX_VERTICES); + ibo = new GPUBuffer (dev, VkBufferUsageFlags.IndexBuffer | VkBufferUsageFlags.StorageBuffer, MAX_VERTICES * 3); + + inBuff = new GPUBuffer (dev, VkBufferUsageFlags.StorageBuffer | VkBufferUsageFlags.TransferSrc | VkBufferUsageFlags.TransferDst, (int)data_size); + outBuff = new GPUBuffer (dev, VkBufferUsageFlags.StorageBuffer | VkBufferUsageFlags.TransferSrc | VkBufferUsageFlags.TransferDst, (int)data_size); + + dsPool = new DescriptorPool (dev, 4, + new VkDescriptorPoolSize (VkDescriptorType.CombinedImageSampler), + new VkDescriptorPoolSize (VkDescriptorType.StorageBuffer, 6)); + dslImage = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler) + ); + dslCompute = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Compute, VkDescriptorType.StorageBuffer), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Compute, VkDescriptorType.StorageBuffer) + ); + dslVAO = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Compute, VkDescriptorType.StorageBuffer), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Compute, VkDescriptorType.StorageBuffer) + ); + + plInit = new ComputePipeline ( + new PipelineLayout (dev, new VkPushConstantRange (VkShaderStageFlags.Compute, 3 * sizeof (int)), dslCompute, dslVAO), + "shaders/init.comp.spv"); + plCompute = new ComputePipeline (plInit.Layout, "shaders/computeTest.comp.spv"); + plNormalize = new ComputePipeline (plInit.Layout, "shaders/normalize.comp.spv"); + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, VkSampleCountFlags.SampleCount1); + + cfg.Layout = new PipelineLayout (dev, dslImage); + cfg.RenderPass = new RenderPass (dev, swapChain.ColorFormat, dev.GetSuitableDepthFormat (), VkSampleCountFlags.SampleCount1); + cfg.RenderPass.ClearValues[0] = new VkClearValue { color = new VkClearColorValue (0.1f, 0.1f, 0.1f) }; + cfg.AddShader (VkShaderStageFlags.Vertex, "shaders/FullScreenQuad.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "shaders/simpletexture.frag.spv"); + + cfg.blendAttachments[0] = new VkPipelineColorBlendAttachmentState (true); + + grPipeline = new GraphicPipeline (cfg); + + cfg.ResetShadersAndVerticesInfos (); + cfg.Layout = new PipelineLayout (dev, new VkPushConstantRange (VkShaderStageFlags.Vertex, 4 * sizeof (int))); + cfg.inputAssemblyState.topology = VkPrimitiveTopology.LineStrip; + cfg.AddVertexBinding (0); + cfg.SetVertexAttributes (0, VkFormat.R32g32Sfloat); + cfg.AddShader (VkShaderStageFlags.Vertex, "shaders/triangle.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "shaders/triangle.frag.spv"); + + trianglesPipeline = new GraphicPipeline (cfg); + + dsImage = dsPool.Allocate (dslImage); + dsPing = dsPool.Allocate (dslCompute); + dsPong = dsPool.Allocate (dslCompute); + dsVAO = dsPool.Allocate (dslCompute); + + + DescriptorSetWrites dsUpdate = new DescriptorSetWrites (dsPing, dslCompute); + dsUpdate.Write (dev, inBuff.Descriptor, outBuff.Descriptor); + dsUpdate.Write (dev, dsPong, outBuff.Descriptor, inBuff.Descriptor); + dsUpdate = new DescriptorSetWrites (dsImage, dslImage); + dsUpdate.Write (dev, imgResult.Descriptor); + dsUpdate = new DescriptorSetWrites (dsVAO, dslVAO); + dsUpdate.Write (dev, vbo.Descriptor, ibo.Descriptor); + + UpdateFrequency = 5; + + addPoint (IMG_DIM / 2 - 1, IMG_DIM / 2 - 1); + } + + protected override void createQueues () { + computeQ = new Queue (dev, VkQueueFlags.Compute); + transferQ = new Queue (dev, VkQueueFlags.Transfer); + + base.createQueues (); + } + + protected override void OnResize () { + + if (frameBuffers != null) + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); + frameBuffers = new Framebuffer[swapChain.ImageCount]; + + for (int i = 0; i < swapChain.ImageCount; ++i) { + frameBuffers[i] = new Framebuffer (grPipeline.RenderPass, swapChain.Width, swapChain.Height, + (grPipeline.Samples == VkSampleCountFlags.SampleCount1) ? new Image[] { + swapChain.images[i], + null + } : new Image[] { + null, + null, + swapChain.images[i] + }); + + cmds[i] = cmdPool.AllocateCommandBuffer (); + cmds[i].Start (); + + imgResult.SetLayout (cmds[i], VkImageAspectFlags.Color, + VkImageLayout.Undefined, VkImageLayout.ShaderReadOnlyOptimal, + VkPipelineStageFlags.AllCommands, VkPipelineStageFlags.FragmentShader); + + grPipeline.RenderPass.Begin (cmds[i], frameBuffers[i]); + + int xPad = (int)swapChain.Width / 2 - (int)IMG_DIM * (int)zoom / 2; + int yPad = (int)swapChain.Height / 2- (int)IMG_DIM * (int)zoom / 2; + + cmds[i].SetViewport (IMG_DIM * zoom, IMG_DIM * zoom, xPad, yPad); + cmds[i].SetScissor (IMG_DIM * zoom, IMG_DIM * zoom, Math.Max (0, xPad), Math.Max (0, yPad)); + + cmds[i].BindDescriptorSet (grPipeline.Layout, dsImage); + cmds[i].BindPipeline (grPipeline); + cmds[i].Draw (3, 1, 0, 0); + + trianglesPipeline.Bind (cmds[i]); + cmds[i].PushConstant (trianglesPipeline.Layout, VkShaderStageFlags.Vertex, IMG_DIM); + cmds[i].PushConstant (trianglesPipeline.Layout, VkShaderStageFlags.Vertex, xPad, sizeof(int)); + cmds[i].PushConstant (trianglesPipeline.Layout, VkShaderStageFlags.Vertex, yPad, 2 * sizeof (int)); + cmds[i].PushConstant (trianglesPipeline.Layout, VkShaderStageFlags.Vertex, zoom, 3 * sizeof (int)); + + cmds[i].BindVertexBuffer (vbo); + cmds[i].BindIndexBuffer (ibo, VkIndexType.Uint32); + cmds[i].DrawIndexed (pointCount*3); + + grPipeline.RenderPass.End (cmds[i]); + + cmds[i].End (); + } + } + + public override void Update () { + initGpuBuffers (); + + using (CommandPool cmdPoolCompute = new CommandPool (dev, computeQ.qFamIndex)) { + + CommandBuffer cmd = cmdPoolCompute.AllocateAndStart (VkCommandBufferUsageFlags.OneTimeSubmit); + + plInit.BindDescriptorSet (cmd, dsVAO, 1); + cmd.PushConstant (plCompute.Layout, VkShaderStageFlags.Compute, IMG_DIM, sizeof (int)); + cmd.PushConstant (plCompute.Layout, VkShaderStageFlags.Compute, pointCount, 2 * sizeof (int)); + + if (!pong) + plInit.BindDescriptorSet (cmd, dsPong); + else + plInit.BindDescriptorSet (cmd, dsPing); + + plInit.Bind (cmd); + cmd.Dispatch (pointCount); + + VkMemoryBarrier memBar = VkMemoryBarrier.New (); + memBar.srcAccessMask = VkAccessFlags.ShaderWrite; + memBar.dstAccessMask = VkAccessFlags.ShaderRead; + Vk.vkCmdPipelineBarrier (cmd.Handle, VkPipelineStageFlags.ComputeShader, VkPipelineStageFlags.ComputeShader, VkDependencyFlags.ByRegion, + 1, ref memBar, 0, IntPtr.Zero, 0, IntPtr.Zero); + + pong = false; + uint stepSize = IMG_DIM / 2; + + plCompute.Bind (cmd); + + + int pass = 0; + while (stepSize > 0 && pass < invocationCount) { + cmd.PushConstant (plCompute.Layout, VkShaderStageFlags.Compute, stepSize); + + if (pong) + plCompute.BindDescriptorSet (cmd, dsPong); + else + plCompute.BindDescriptorSet (cmd, dsPing); + + cmd.Dispatch (IMG_DIM, IMG_DIM); + + Vk.vkCmdPipelineBarrier (cmd.Handle, VkPipelineStageFlags.ComputeShader, VkPipelineStageFlags.ComputeShader, VkDependencyFlags.ByRegion, + 1, ref memBar, 0, IntPtr.Zero, 0, IntPtr.Zero); + + pong = !pong; + stepSize /= 2; + pass++; + } + + plNormalize.Bind (cmd); + if (pong) + plNormalize.BindDescriptorSet (cmd, dsPong); + else + plNormalize.BindDescriptorSet (cmd, dsPing); + cmd.Dispatch (IMG_DIM, IMG_DIM); + pong = !pong; + + cmd.End (); + + computeQ.Submit (cmd); + computeQ.WaitIdle (); + } + + printResults (); + } + + protected override void onMouseButtonDown (MouseButton button) { + int xPad = (int)swapChain.Width / 2 - (int)IMG_DIM * (int)zoom / 2; + int yPad = (int)swapChain.Height / 2 - (int)IMG_DIM * (int)zoom / 2; + + int localX = (int)((lastMouseX - xPad) / zoom); + int localY = (int)((lastMouseY - yPad) / zoom); + + if (localX < 0 || localY < 0 || localX >= IMG_DIM || localY >= IMG_DIM) + base.onMouseButtonDown (button); + else { + addPoint ((uint)localX, (uint)localY); + } + } + protected override void onKeyDown (Key key, int scanCode, Modifier modifiers) { + switch (key) { + case Key.Delete: + clearPoints (); + break; + case Key.KeypadAdd: + invocationCount++; + break; + case Key.KeypadSubtract: + if (invocationCount>0) + invocationCount--; + break; + default: + base.onKeyDown (key, scanCode, modifiers); + break; + } + Console.WriteLine ($"break after {invocationCount} step"); + } + + void printResults () { + dev.WaitIdle (); + using (CommandPool cmdPoolTransfer = new CommandPool (dev, transferQ.qFamIndex)) { + + CommandBuffer cmd = cmdPoolTransfer.AllocateAndStart (VkCommandBufferUsageFlags.OneTimeSubmit); + + imgResult.SetLayout (cmd, VkImageAspectFlags.Color, + VkImageLayout.ShaderReadOnlyOptimal, VkImageLayout.TransferDstOptimal, + VkPipelineStageFlags.FragmentShader, VkPipelineStageFlags.Transfer); + + if (pong) + outBuff.CopyTo (cmd, imgResult, VkImageLayout.ShaderReadOnlyOptimal); + else + inBuff.CopyTo (cmd, imgResult, VkImageLayout.ShaderReadOnlyOptimal); + + cmd.End (); + + transferQ.Submit (cmd); + transferQ.WaitIdle (); + } + } + + void initGpuBuffers () { + using (CommandPool staggingCmdPool = new CommandPool (dev, transferQ.qFamIndex)) { + CommandBuffer cmd = staggingCmdPool.AllocateAndStart (VkCommandBufferUsageFlags.OneTimeSubmit); + + if (clear) { + if (pong) + inBuff.Fill (cmd, 0); + else + outBuff.Fill (cmd, 0); + } + + staggingVBO.CopyTo (cmd, vbo, pointCount * (ulong)Marshal.SizeOf()); + + transferQ.EndSubmitAndWait (cmd); + } + } + + protected override void Dispose (bool disposing) { + if (disposing) { + if (!isDisposed) { + dev.WaitIdle (); + + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); + + grPipeline.Dispose (); + trianglesPipeline.Dispose (); + + plInit.Dispose (); + plCompute.Dispose (); + plNormalize.Dispose (); + + dslCompute.Dispose (); + dslImage.Dispose (); + + dsPool.Dispose (); + + inBuff.Dispose (); + outBuff.Dispose (); + staggingVBO.Dispose (); + vbo.Dispose (); + ibo.Dispose (); + + imgResult.Dispose (); + +#if DEBUG + dbgReport.Dispose (); +#endif + } + } + + base.Dispose (disposing); + } + + + } +} diff --git a/samples/deferred/DebuDrawPipeline.cs b/samples/deferred/DebuDrawPipeline.cs new file mode 100644 index 0000000..fef2751 --- /dev/null +++ b/samples/deferred/DebuDrawPipeline.cs @@ -0,0 +1,78 @@ +using System; +using System.Numerics; +using System.Runtime.InteropServices; +using VK; + +namespace CVKL { + public class DebugDrawPipeline : GraphicPipeline { + public HostBuffer Vertices; + uint vertexCount; + uint vboLength = 100 * 6 * sizeof (float); + + public DebugDrawPipeline (Device dev, DescriptorSetLayout dsLayout, VkFormat colorFormat, VkSampleCountFlags samples = VkSampleCountFlags.SampleCount1) : + base (new RenderPass (dev, colorFormat), "Debug draw pipeline") { + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.LineList, samples); + cfg.rasterizationState.lineWidth = 1.0f; + cfg.RenderPass = RenderPass; + cfg.Layout = new PipelineLayout(dev, dsLayout); + cfg.Layout.AddPushConstants ( + new VkPushConstantRange (VkShaderStageFlags.Vertex, (uint)Marshal.SizeOf () * 2) + ); + cfg.AddVertexBinding (0, 6 * sizeof(float)); + cfg.SetVertexAttributes (0, VkFormat.R32g32b32Sfloat, VkFormat.R32g32b32Sfloat); + + cfg.blendAttachments[0] = new VkPipelineColorBlendAttachmentState (true); + + cfg.AddShader (VkShaderStageFlags.Vertex, "shaders/debug.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "shaders/debug.frag.spv"); + + layout = cfg.Layout; + + init (cfg); + + Vertices = new HostBuffer (dev, VkBufferUsageFlags.VertexBuffer, vboLength); + Vertices.Map (); + } + + public void AddLine (Vector3 start, Vector3 end, float r, float g, float b) { + float[] data = { + start.X, start.Y, start.Z, + r, g, b, + end.X, end.Y, end.Z, + r, g, b + }; + Vertices.Update (data, 12 * sizeof (float), vertexCount * 6 * sizeof (float)); + vertexCount+=2; + } + + public void RecordDraw (CommandBuffer cmd, Framebuffer fb, Camera camera) { + RenderPass.Begin (cmd, fb); + const int ratio = 8; + cmd.SetViewport (fb.Width/ratio, fb.Height/ratio, (ratio-1) * (int)fb.Width / ratio, (ratio-1) * (int)fb.Height / ratio); + //cmd.SetViewport (200, 200,100,100,-10,10);//, 4 * (int)fb.Width / 5, 4 * (int)fb.Height / 5); + cmd.SetScissor (fb.Width / ratio, fb.Height / ratio, (ratio-1) * (int)fb.Width / ratio, (ratio-1) * (int)fb.Height / ratio); + //cmd.SetScissor (200, 200,100,100); + + Matrix4x4 ortho = Matrix4x4.CreateOrthographic (4, 4.0f / camera.AspectRatio,-1,1); + + cmd.PushConstant (layout, VkShaderStageFlags.Vertex, ortho); + + Bind (cmd); + + cmd.BindVertexBuffer (Vertices); + cmd.Draw (vertexCount); + RenderPass.End (cmd); + } + + protected override void Dispose (bool disposing) { + if (disposing) { + Vertices.Unmap (); + Vertices.Dispose (); + } + + base.Dispose (disposing); + } + } + +} diff --git a/samples/deferred/DeferredPbrRenderer.cs b/samples/deferred/DeferredPbrRenderer.cs new file mode 100644 index 0000000..3dc8520 --- /dev/null +++ b/samples/deferred/DeferredPbrRenderer.cs @@ -0,0 +1,528 @@ +using System; +using System.Numerics; +using System.Runtime.InteropServices; +using CVKL; +using CVKL.glTF; +using VK; + +namespace deferred { + public class DeferredPbrRenderer : IDisposable { + Device dev; + Queue gQueue; + public static int MAX_MATERIAL_COUNT = 4; + public static VkSampleCountFlags NUM_SAMPLES = VkSampleCountFlags.SampleCount1; + public static VkFormat HDR_FORMAT = VkFormat.R16g16b16a16Sfloat; + public static VkFormat MRT_FORMAT = VkFormat.R32g32b32a32Sfloat; + public static bool TEXTURE_ARRAY; + + public enum DebugView { + none, + color, + normal, + pos, + occlusion, + emissive, + metallic, + roughness, + depth, + prefill, + irradiance, + shadowMap + } + public DebugView currentDebugView = DebugView.none; + public int lightNumDebug = 0; + public int debugMip = 0; + public int debugFace = 0; + + const float lightMoveSpeed = 0.1f; + public float exposure = 2.0f; + public float gamma = 1.2f; + + public struct Matrices { + public Matrix4x4 projection; + public Matrix4x4 model; + public Matrix4x4 view; + public Vector4 camPos; + public float prefilteredCubeMipLevels; + public float scaleIBLAmbient; + } + public struct Light { + public Vector4 position; + public Vector4 color; + public Matrix4x4 mvp; + } + + public Matrices matrices = new Matrices { + scaleIBLAmbient = 0.5f, + }; + public Light[] lights = { + new Light { + position = new Vector4(2.5f,5.5f,2,0f), + color = new Vector4(1,0.8f,0.8f,1) + }, + /*new Light { + position = new Vector4(-2.5f,5.5f,2,0f), + color = new Vector4(0.8f,0.8f,1,1) + }*/ + }; + + Framebuffer frameBuffer; + public Image gbColorRough, gbEmitMetal, gbN_AO, gbPos, hdrImgResolved, hdrImgMS; + + DescriptorPool descriptorPool; + DescriptorSetLayout descLayoutMain, descLayoutTextures, descLayoutGBuff; + DescriptorSet dsMain, dsGBuff; + + public PipelineCache pipelineCache; + Pipeline gBuffPipeline, composePipeline, debugPipeline; + + public HostBuffer uboMatrices { get; private set; } + public HostBuffer uboLights { get; private set; } + + RenderPass renderPass; + + public PbrModel model { get; private set; } + public EnvironmentCube envCube; + public ShadowMapRenderer shadowMapRenderer; + + public BoundingBox modelAABB; + + public VkSemaphore DrawComplete; + + const int SP_SKYBOX = 0; + const int SP_MODELS = 1; + const int SP_COMPOSE = 2; + //const int SP_TONE_MAPPING = 3; + + string cubemapPath; + + uint width, height; + public uint Width => width; + public uint Height => height; + + public DeferredPbrRenderer (Queue gQueue, string cubemapPath, uint width, uint height, float nearPlane, float farPlane) { + this.gQueue = gQueue; + this.dev = gQueue.Dev; + this.cubemapPath = cubemapPath; + this.width = width; + this.height = height; + + DrawComplete = dev.CreateSemaphore(); + + pipelineCache = new PipelineCache (dev); + + descriptorPool = new DescriptorPool (dev, 5, + new VkDescriptorPoolSize (VkDescriptorType.UniformBuffer, 3), + new VkDescriptorPoolSize (VkDescriptorType.CombinedImageSampler, 6), + new VkDescriptorPoolSize (VkDescriptorType.InputAttachment, 5), + new VkDescriptorPoolSize (VkDescriptorType.StorageImage, 4) + ); + + uboMatrices = new HostBuffer (dev, VkBufferUsageFlags.UniformBuffer, matrices, true); + uboLights = new HostBuffer (dev, VkBufferUsageFlags.UniformBuffer, lights, true); + +#if WITH_SHADOWS + shadowMapRenderer = new ShadowMapRenderer (gQueue, this); +#endif + + init (nearPlane, farPlane); + } + + void init_renderpass () { + renderPass = new RenderPass (dev, NUM_SAMPLES); + + renderPass.AddAttachment (HDR_FORMAT, VkImageLayout.ColorAttachmentOptimal, VkSampleCountFlags.SampleCount1);//final outpout + renderPass.AddAttachment (dev.GetSuitableDepthFormat (), VkImageLayout.DepthStencilAttachmentOptimal, NUM_SAMPLES); + renderPass.AddAttachment (VkFormat.R8g8b8a8Unorm, VkImageLayout.ColorAttachmentOptimal, NUM_SAMPLES, VkAttachmentLoadOp.Clear, VkAttachmentStoreOp.DontCare);//GBuff0 (color + roughness) and final color before resolve + renderPass.AddAttachment (VkFormat.R8g8b8a8Unorm, VkImageLayout.ColorAttachmentOptimal, NUM_SAMPLES, VkAttachmentLoadOp.Clear, VkAttachmentStoreOp.DontCare);//GBuff1 (emit + metal) + renderPass.AddAttachment (MRT_FORMAT, VkImageLayout.ColorAttachmentOptimal, NUM_SAMPLES, VkAttachmentLoadOp.Clear, VkAttachmentStoreOp.DontCare);//GBuff2 (normals + AO) + renderPass.AddAttachment (MRT_FORMAT, VkImageLayout.ColorAttachmentOptimal, NUM_SAMPLES, VkAttachmentLoadOp.Clear, VkAttachmentStoreOp.DontCare);//GBuff3 (Pos + depth) + if (NUM_SAMPLES != VkSampleCountFlags.SampleCount1) + renderPass.AddAttachment (HDR_FORMAT, VkImageLayout.ColorAttachmentOptimal, NUM_SAMPLES);//hdr color multisampled + + renderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0.0f, 0.0f, 0.0f) }); + renderPass.ClearValues.Add (new VkClearValue { depthStencil = new VkClearDepthStencilValue (1.0f, 0) }); + renderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0.0f, 0.0f, 0.0f) }); + renderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0.0f, 0.0f, 0.0f) }); + renderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0.0f, 0.0f, 0.0f) }); + renderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0.0f, 0.0f, 0.0f) }); + if (NUM_SAMPLES != VkSampleCountFlags.SampleCount1) + renderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0.0f, 0.0f, 0.0f) }); + + uint mainHdr = NUM_SAMPLES == VkSampleCountFlags.SampleCount1 ? 0u : 6u; + + SubPass[] subpass = { new SubPass (), new SubPass (), new SubPass ()}; + //skybox + subpass[SP_SKYBOX].AddColorReference (mainHdr, VkImageLayout.ColorAttachmentOptimal); + //models + subpass[SP_MODELS].AddColorReference (new VkAttachmentReference (2, VkImageLayout.ColorAttachmentOptimal), + new VkAttachmentReference (3, VkImageLayout.ColorAttachmentOptimal), + new VkAttachmentReference (4, VkImageLayout.ColorAttachmentOptimal), + new VkAttachmentReference (5, VkImageLayout.ColorAttachmentOptimal)); + subpass[SP_MODELS].SetDepthReference (1, VkImageLayout.DepthStencilAttachmentOptimal); + subpass[SP_MODELS].AddPreservedReference (mainHdr); + + //compose + subpass[SP_COMPOSE].AddColorReference (mainHdr, VkImageLayout.ColorAttachmentOptimal); + subpass[SP_COMPOSE].AddInputReference (new VkAttachmentReference (2, VkImageLayout.ShaderReadOnlyOptimal), + new VkAttachmentReference (3, VkImageLayout.ShaderReadOnlyOptimal), + new VkAttachmentReference (4, VkImageLayout.ShaderReadOnlyOptimal), + new VkAttachmentReference (5, VkImageLayout.ShaderReadOnlyOptimal)); + if (NUM_SAMPLES != VkSampleCountFlags.SampleCount1) + subpass[SP_COMPOSE].AddResolveReference (0, VkImageLayout.ColorAttachmentOptimal); + //tone mapping + //subpass[SP_TONE_MAPPING].AddColorReference ((NUM_SAMPLES == VkSampleCountFlags.SampleCount1) ? 0u : 2u, VkImageLayout.ColorAttachmentOptimal); + //subpass[SP_TONE_MAPPING].AddInputReference (new VkAttachmentReference (6, VkImageLayout.ShaderReadOnlyOptimal)); + //if (NUM_SAMPLES != VkSampleCountFlags.SampleCount1) + //subpass[SP_TONE_MAPPING].AddResolveReference (0, VkImageLayout.ColorAttachmentOptimal); + + renderPass.AddSubpass (subpass); + + renderPass.AddDependency (Vk.SubpassExternal, SP_SKYBOX, + VkPipelineStageFlags.BottomOfPipe, VkPipelineStageFlags.ColorAttachmentOutput, + VkAccessFlags.MemoryRead, VkAccessFlags.ColorAttachmentRead | VkAccessFlags.ColorAttachmentWrite); + renderPass.AddDependency (SP_SKYBOX, SP_MODELS, + VkPipelineStageFlags.ColorAttachmentOutput, VkPipelineStageFlags.FragmentShader, + VkAccessFlags.ColorAttachmentWrite, VkAccessFlags.ShaderRead); + renderPass.AddDependency (SP_MODELS, SP_COMPOSE, + VkPipelineStageFlags.ColorAttachmentOutput, VkPipelineStageFlags.FragmentShader, + VkAccessFlags.ColorAttachmentWrite, VkAccessFlags.ShaderRead); + //renderPass.AddDependency (SP_COMPOSE, Vk.SubpassExternal, + //VkPipelineStageFlags.ColorAttachmentOutput, VkPipelineStageFlags.Transfer, + //VkAccessFlags.ColorAttachmentWrite, VkAccessFlags.TransferRead); + //renderPass.AddDependency (SP_COMPOSE, SP_COMPOSE, + //VkPipelineStageFlags.Transfer, VkPipelineStageFlags.ComputeShader, + //VkAccessFlags.TransferWrite, VkAccessFlags.ShaderRead); + //renderPass.AddDependency (Vk.SubpassExternal, SP_TONE_MAPPING, + // VkPipelineStageFlags.ComputeShader, VkPipelineStageFlags.FragmentShader, + // VkAccessFlags.ShaderWrite, VkAccessFlags.ShaderRead); + //renderPass.AddDependency (SP_SKYBOX, SP_TONE_MAPPING, + //VkPipelineStageFlags.ColorAttachmentOutput, VkPipelineStageFlags.FragmentShader, + //VkAccessFlags.ColorAttachmentWrite, VkAccessFlags.ShaderRead); + renderPass.AddDependency (SP_COMPOSE, Vk.SubpassExternal, + VkPipelineStageFlags.ColorAttachmentOutput, VkPipelineStageFlags.Transfer, + VkAccessFlags.ColorAttachmentRead | VkAccessFlags.ColorAttachmentWrite, VkAccessFlags.TransferRead); + //renderPass.AddDependency (SP_TONE_MAPPING, Vk.SubpassExternal, + //VkPipelineStageFlags.ColorAttachmentOutput, VkPipelineStageFlags.BottomOfPipe, + //VkAccessFlags.ColorAttachmentRead | VkAccessFlags.ColorAttachmentWrite, VkAccessFlags.MemoryRead); + } + + void init (float nearPlane, float farPlane) { + init_renderpass (); + + descLayoutMain = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, VkDescriptorType.UniformBuffer),//matrices and params + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (2, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (3, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (4, VkShaderStageFlags.Fragment, VkDescriptorType.UniformBuffer),//lights + new VkDescriptorSetLayoutBinding (5, VkShaderStageFlags.Fragment, VkDescriptorType.UniformBuffer));//materials +#if WITH_SHADOWS + descLayoutMain.Bindings.Add (new VkDescriptorSetLayoutBinding (6, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler)); +#endif + + if (TEXTURE_ARRAY) { + descLayoutMain.Bindings.Add (new VkDescriptorSetLayoutBinding (7, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler));//texture array + //descLayoutMain.Bindings.Add (new VkDescriptorSetLayoutBinding (8, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler));//down sampled hdr + } else { + descLayoutTextures = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (2, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (3, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (4, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler) + ); + } + + descLayoutGBuff = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Fragment, VkDescriptorType.InputAttachment),//color + roughness + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Fragment, VkDescriptorType.InputAttachment),//emit + metal + new VkDescriptorSetLayoutBinding (2, VkShaderStageFlags.Fragment, VkDescriptorType.InputAttachment),//normals + AO + new VkDescriptorSetLayoutBinding (3, VkShaderStageFlags.Fragment, VkDescriptorType.InputAttachment));//Pos + depth + + + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, NUM_SAMPLES); + cfg.rasterizationState.cullMode = VkCullModeFlags.Back; + if (NUM_SAMPLES != VkSampleCountFlags.SampleCount1) { + cfg.multisampleState.sampleShadingEnable = true; + cfg.multisampleState.minSampleShading = 0.5f; + } + cfg.Cache = pipelineCache; + if (TEXTURE_ARRAY) + cfg.Layout = new PipelineLayout (dev, descLayoutMain, descLayoutGBuff); + else + cfg.Layout = new PipelineLayout (dev, descLayoutMain, descLayoutGBuff, descLayoutTextures); + + cfg.Layout.AddPushConstants ( + new VkPushConstantRange (VkShaderStageFlags.Vertex, (uint)Marshal.SizeOf ()), + new VkPushConstantRange (VkShaderStageFlags.Fragment, sizeof (int), 64) + ); + cfg.RenderPass = renderPass; + cfg.SubpassIndex = SP_MODELS; + cfg.blendAttachments.Add (new VkPipelineColorBlendAttachmentState (false)); + cfg.blendAttachments.Add (new VkPipelineColorBlendAttachmentState (false)); + cfg.blendAttachments.Add (new VkPipelineColorBlendAttachmentState (false)); + //cfg.blendAttachments.Add (new VkPipelineColorBlendAttachmentState (false)); + + cfg.AddVertex (); + + using (SpecializationInfo constants = new SpecializationInfo ( + new SpecializationConstant (0, nearPlane), + new SpecializationConstant (1, farPlane), + new SpecializationConstant (2, MAX_MATERIAL_COUNT))) { + + cfg.AddShader (VkShaderStageFlags.Vertex, "#deferred.GBuffPbr.vert.spv"); + if (TEXTURE_ARRAY) + cfg.AddShader (VkShaderStageFlags.Fragment, "#deferred.GBuffPbrTexArray.frag.spv", constants); + else + cfg.AddShader (VkShaderStageFlags.Fragment, "#deferred.GBuffPbr.frag.spv", constants); + + gBuffPipeline = new GraphicPipeline (cfg); + } + cfg.rasterizationState.cullMode = VkCullModeFlags.Front; + //COMPOSE PIPELINE + cfg.blendAttachments.Clear (); + cfg.blendAttachments.Add (new VkPipelineColorBlendAttachmentState (false)); + cfg.ResetShadersAndVerticesInfos (); + cfg.SubpassIndex = SP_COMPOSE; + cfg.Layout = gBuffPipeline.Layout; + cfg.depthStencilState.depthTestEnable = false; + cfg.depthStencilState.depthWriteEnable = false; + using (SpecializationInfo constants = new SpecializationInfo ( + new SpecializationConstant (0, (uint)lights.Length))) { + cfg.AddShader (VkShaderStageFlags.Vertex, "#deferred.FullScreenQuad.vert.spv"); +#if WITH_SHADOWS + cfg.AddShader (VkShaderStageFlags.Fragment, "#deferred.compose_with_shadows.frag.spv", constants); +#else + cfg.AddShader (VkShaderStageFlags.Fragment, "#deferred.compose.frag.spv", constants); +#endif + composePipeline = new GraphicPipeline (cfg); + } + //DEBUG DRAW use subpass of compose + cfg.shaders[1] = new ShaderInfo (VkShaderStageFlags.Fragment, "#deferred.show_gbuff.frag.spv"); + cfg.SubpassIndex = SP_COMPOSE; + debugPipeline = new GraphicPipeline (cfg); + ////TONE MAPPING + //cfg.shaders[1] = new ShaderInfo (VkShaderStageFlags.Fragment, "#deferred.tone_mapping.frag.spv"); + //cfg.SubpassIndex = SP_TONE_MAPPING; + //toneMappingPipeline = new GraphicPipeline (cfg); + + dsMain = descriptorPool.Allocate (descLayoutMain); + dsGBuff = descriptorPool.Allocate (descLayoutGBuff); + + envCube = new EnvironmentCube (cubemapPath, dsMain, gBuffPipeline.Layout, gQueue, renderPass); + + matrices.prefilteredCubeMipLevels = envCube.prefilterCube.CreateInfo.mipLevels; + + DescriptorSetWrites dsMainWrite = new DescriptorSetWrites (dsMain, descLayoutMain.Bindings.GetRange (0, 5).ToArray ()); + dsMainWrite.Write (dev, + uboMatrices.Descriptor, + envCube.irradianceCube.Descriptor, + envCube.prefilterCube.Descriptor, + envCube.lutBrdf.Descriptor, + uboLights.Descriptor); + +#if WITH_SHADOWS + dsMainWrite = new DescriptorSetWrites (dsMain, descLayoutMain.Bindings[6]); + dsMainWrite.Write (dev, shadowMapRenderer.shadowMap.Descriptor); +#endif + } + + + public void LoadModel (Queue transferQ, string path) { + dev.WaitIdle (); + model?.Dispose (); + + if (TEXTURE_ARRAY) { + PbrModelTexArray mod = new PbrModelTexArray (transferQ, path); + if (mod.texArray != null) { + DescriptorSetWrites uboUpdate = new DescriptorSetWrites (dsMain, descLayoutMain.Bindings[5], descLayoutMain.Bindings[7]); + uboUpdate.Write (dev, mod.materialUBO.Descriptor, mod.texArray.Descriptor); + } + model = mod; + } else { + model = new PbrModelSeparatedTextures (transferQ, path, + descLayoutTextures, + AttachmentType.Color, + AttachmentType.PhysicalProps, + AttachmentType.Normal, + AttachmentType.AmbientOcclusion, + AttachmentType.Emissive); + + DescriptorSetWrites uboUpdate = new DescriptorSetWrites (dsMain, descLayoutMain.Bindings[5]); + uboUpdate.Write (dev, model.materialUBO.Descriptor); + } + + + modelAABB = model.DefaultScene.AABB; + } + public void buildCommandBuffers (CommandBuffer cmd) { + + + renderPass.Begin (cmd, frameBuffer); + + cmd.SetViewport (frameBuffer.Width, frameBuffer.Height); + cmd.SetScissor (frameBuffer.Width, frameBuffer.Height); + + cmd.BindDescriptorSet (gBuffPipeline.Layout, dsMain); + + envCube.RecordDraw (cmd); + + renderPass.BeginSubPass (cmd); + + if (model != null) { + gBuffPipeline.Bind (cmd); + model.Bind (cmd); + model.DrawAll (cmd, gBuffPipeline.Layout); + } + + renderPass.BeginSubPass (cmd); + + cmd.BindDescriptorSet (composePipeline.Layout, dsGBuff, 1); + + if (currentDebugView == DebugView.none) + composePipeline.Bind (cmd); + else { + debugPipeline.Bind (cmd); + uint debugValue = (uint)currentDebugView - 1; + if (currentDebugView == DebugView.shadowMap) + debugValue += (uint)((lightNumDebug << 8)); + else + debugValue += (uint)((debugFace << 8) + (debugMip << 16)); + cmd.PushConstant (debugPipeline.Layout, VkShaderStageFlags.Fragment, debugValue, (uint)Marshal.SizeOf ()); + } + + cmd.Draw (3, 1, 0, 0); + + //renderPass.BeginSubPass (cmd); + //toneMappingPipeline.Bind (cmd); + //cmd.Draw (3, 1, 0, 0); + + renderPass.End (cmd); + } + + public void MoveLight (Vector4 dir) { + lights[lightNumDebug].position += dir * lightMoveSpeed; +#if WITH_SHADOWS + shadowMapRenderer.updateShadowMap = true; +#endif + } + + #region update + public void UpdateView (Camera camera) { + camera.AspectRatio = (float)width / height; + + matrices.projection = camera.Projection; + matrices.view = camera.View; + matrices.model = camera.Model; + + matrices.camPos = new Vector4 ( + -camera.Position.Z * (float)Math.Sin (camera.Rotation.Y) * (float)Math.Cos (camera.Rotation.X), + camera.Position.Z * (float)Math.Sin (camera.Rotation.X), + camera.Position.Z * (float)Math.Cos (camera.Rotation.Y) * (float)Math.Cos (camera.Rotation.X), + 0 + ); + + uboMatrices.Update (matrices, (uint)Marshal.SizeOf ()); + } + + #endregion + + + void createGBuff () { + gbColorRough?.Dispose (); + gbEmitMetal?.Dispose (); + gbN_AO?.Dispose (); + gbPos?.Dispose (); + hdrImgResolved?.Dispose (); + hdrImgMS?.Dispose (); + + + hdrImgResolved = new Image (dev, HDR_FORMAT, VkImageUsageFlags.Sampled | VkImageUsageFlags.ColorAttachment | VkImageUsageFlags.TransferSrc, VkMemoryPropertyFlags.DeviceLocal, width, height, VkImageType.Image2D, VkSampleCountFlags.SampleCount1); + gbColorRough = new Image (dev, VkFormat.R8g8b8a8Unorm, VkImageUsageFlags.InputAttachment | VkImageUsageFlags.ColorAttachment | VkImageUsageFlags.TransientAttachment, VkMemoryPropertyFlags.DeviceLocal, width, height, VkImageType.Image2D, NUM_SAMPLES); + gbEmitMetal = new Image (dev, VkFormat.R8g8b8a8Unorm, VkImageUsageFlags.InputAttachment | VkImageUsageFlags.ColorAttachment | VkImageUsageFlags.TransientAttachment, VkMemoryPropertyFlags.DeviceLocal, width, height, VkImageType.Image2D, NUM_SAMPLES); + gbN_AO = new Image (dev, MRT_FORMAT, VkImageUsageFlags.InputAttachment | VkImageUsageFlags.ColorAttachment | VkImageUsageFlags.TransientAttachment, VkMemoryPropertyFlags.DeviceLocal, width, height, VkImageType.Image2D, NUM_SAMPLES); + gbPos = new Image (dev, MRT_FORMAT, VkImageUsageFlags.InputAttachment | VkImageUsageFlags.ColorAttachment | VkImageUsageFlags.TransientAttachment, VkMemoryPropertyFlags.DeviceLocal, width, height, VkImageType.Image2D, NUM_SAMPLES); + if (NUM_SAMPLES != VkSampleCountFlags.SampleCount1) + hdrImgMS = new Image (dev, HDR_FORMAT, VkImageUsageFlags.ColorAttachment | VkImageUsageFlags.TransientAttachment, VkMemoryPropertyFlags.DeviceLocal, width, height, VkImageType.Image2D, NUM_SAMPLES); + + + gbColorRough.CreateView (); gbColorRough.CreateSampler (); + gbColorRough.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + gbEmitMetal.CreateView (); gbEmitMetal.CreateSampler (); + gbEmitMetal.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + gbN_AO.CreateView (); gbN_AO.CreateSampler (); + gbN_AO.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + gbPos.CreateView (); gbPos.CreateSampler (); + gbPos.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + hdrImgResolved.CreateView (); hdrImgResolved.CreateSampler (); + hdrImgResolved.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + + hdrImgMS?.CreateView (); + + + DescriptorSetWrites uboUpdate = new DescriptorSetWrites (descLayoutGBuff); + uboUpdate.Write (dev, dsGBuff, gbColorRough.Descriptor, + gbEmitMetal.Descriptor, + gbN_AO.Descriptor, + gbPos.Descriptor); + + gbColorRough.SetName ("GBuffColorRough"); + gbEmitMetal.SetName ("GBuffEmitMetal"); + gbN_AO.SetName ("GBuffN"); + gbPos.SetName ("GBuffPos"); + hdrImgResolved.SetName ("HDRimg resolved"); + hdrImgMS?.SetName ("HDRimg resolved"); + } + + public void Resize (uint width, uint height) { + this.width = width; + this.height = height; + + frameBuffer?.Dispose (); + createGBuff (); + + frameBuffer = (NUM_SAMPLES == VkSampleCountFlags.SampleCount1) ? + new Framebuffer (renderPass, width, height, new Image[] { + hdrImgResolved, null, gbColorRough, gbEmitMetal, gbN_AO, gbPos}) : + new Framebuffer (renderPass, width, height, new Image[] { + hdrImgResolved, null, gbColorRough, gbEmitMetal, gbN_AO, gbPos, hdrImgMS}); + } + + public void Dispose () { + dev.WaitIdle (); + + frameBuffer?.Dispose (); + + gbColorRough.Dispose (); + gbEmitMetal.Dispose (); + gbN_AO.Dispose (); + gbPos.Dispose (); + hdrImgMS?.Dispose (); + hdrImgResolved.Dispose (); + + gBuffPipeline.Dispose (); + composePipeline.Dispose (); + //toneMappingPipeline.Dispose (); + debugPipeline?.Dispose (); + + descLayoutMain.Dispose (); + descLayoutTextures?.Dispose (); + descLayoutGBuff.Dispose (); + + uboMatrices.Dispose (); + uboLights.Dispose (); + model.Dispose (); + envCube.Dispose (); + +#if WITH_SHADOWS + shadowMapRenderer.Dispose (); +#endif + + descriptorPool.Dispose (); + + dev.DestroySemaphore (DrawComplete); + } + } +} diff --git a/samples/deferred/EnvironmentPipeline.cs b/samples/deferred/EnvironmentPipeline.cs new file mode 100644 index 0000000..4e97a1c --- /dev/null +++ b/samples/deferred/EnvironmentPipeline.cs @@ -0,0 +1,318 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.InteropServices; +using VK; + +namespace CVKL { + public class EnvironmentCube : GraphicPipeline { + + GPUBuffer vboSkybox; + + public Image cubemap { get; private set; } + public Image lutBrdf { get; private set; } + public Image irradianceCube { get; private set; } + public Image prefilterCube { get; set; } + + public EnvironmentCube (string cubemapPath, DescriptorSet dsSkybox, PipelineLayout plLayout, Queue staggingQ, RenderPass renderPass, PipelineCache cache = null) + : base (renderPass, cache, "EnvCube pipeline") { + + using (CommandPool cmdPool = new CommandPool (staggingQ.Dev, staggingQ.index)) { + + vboSkybox = new GPUBuffer (staggingQ, cmdPool, VkBufferUsageFlags.VertexBuffer, box_vertices); + + cubemap = KTX.KTX.Load (staggingQ, cmdPool, cubemapPath, + VkImageUsageFlags.Sampled, VkMemoryPropertyFlags.DeviceLocal, true); + cubemap.CreateView (VkImageViewType.Cube, VkImageAspectFlags.Color); + cubemap.CreateSampler (VkSamplerAddressMode.ClampToEdge); + cubemap.SetName ("skybox Texture"); + cubemap.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, renderPass.Samples, false); + cfg.RenderPass = renderPass; + cfg.Layout = plLayout; + cfg.AddVertexBinding (0, 3 * sizeof (float)); + cfg.AddVertexAttributes (0, VkFormat.R32g32b32Sfloat); + cfg.AddShader (VkShaderStageFlags.Vertex, "#deferred.skybox.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "#deferred.skybox.frag.spv"); + cfg.multisampleState.rasterizationSamples = Samples; + + layout = cfg.Layout; + + init (cfg); + + generateBRDFLUT (staggingQ, cmdPool); + generateCubemaps (staggingQ, cmdPool); + } + + } + + public void RecordDraw (CommandBuffer cmd) { + Bind (cmd); + cmd.BindVertexBuffer (vboSkybox); + cmd.Draw (36); + } + + #region skybox + + static float[] box_vertices = { + 1.0f, 1.0f,-1.0f, // +X side + 1.0f, 1.0f, 1.0f, + 1.0f,-1.0f, 1.0f, + 1.0f,-1.0f, 1.0f, + 1.0f,-1.0f,-1.0f, + 1.0f, 1.0f,-1.0f, + + -1.0f,-1.0f,-1.0f, // +X side + -1.0f,-1.0f, 1.0f, + -1.0f, 1.0f, 1.0f, + -1.0f, 1.0f, 1.0f, + -1.0f, 1.0f,-1.0f, + -1.0f,-1.0f,-1.0f, + + -1.0f, 1.0f,-1.0f, // +Y side + -1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + -1.0f, 1.0f,-1.0f, + 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f,-1.0f, + + -1.0f,-1.0f,-1.0f, // -Y side + 1.0f,-1.0f,-1.0f, + 1.0f,-1.0f, 1.0f, + -1.0f,-1.0f,-1.0f, + 1.0f,-1.0f, 1.0f, + -1.0f,-1.0f, 1.0f, + + -1.0f, 1.0f, 1.0f, // +Z side + -1.0f,-1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + -1.0f,-1.0f, 1.0f, + 1.0f,-1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + + -1.0f,-1.0f,-1.0f, // -Z side + 1.0f, 1.0f,-1.0f, + 1.0f,-1.0f,-1.0f, + -1.0f,-1.0f,-1.0f, + -1.0f, 1.0f,-1.0f, + 1.0f, 1.0f,-1.0f, + + }; + #endregion + + void generateBRDFLUT (Queue staggingQ, CommandPool cmdPool) { + const VkFormat format = VkFormat.R16g16Sfloat; + const int dim = 512; + + lutBrdf = new Image (Dev, format, VkImageUsageFlags.ColorAttachment | VkImageUsageFlags.Sampled, + VkMemoryPropertyFlags.DeviceLocal, dim, dim); + lutBrdf.SetName ("lutBrdf"); + + lutBrdf.CreateView (); + lutBrdf.CreateSampler (VkSamplerAddressMode.ClampToEdge); + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, VkSampleCountFlags.SampleCount1, false); + + cfg.Layout = new PipelineLayout (Dev, new DescriptorSetLayout (Dev)); + cfg.RenderPass = new RenderPass (Dev); + cfg.RenderPass.AddAttachment (format, VkImageLayout.ShaderReadOnlyOptimal); + cfg.RenderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0, 0, 0) }); + cfg.RenderPass.AddSubpass (new SubPass (VkImageLayout.ColorAttachmentOptimal)); + cfg.AddShader (VkShaderStageFlags.Vertex, "#deferred.genbrdflut.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "#deferred.genbrdflut.frag.spv"); + + using (GraphicPipeline pl = new GraphicPipeline (cfg)) { + using (Framebuffer fb = new Framebuffer (cfg.RenderPass, dim, dim, lutBrdf)) { + CommandBuffer cmd = cmdPool.AllocateCommandBuffer (); + cmd.Start (VkCommandBufferUsageFlags.OneTimeSubmit); + pl.RenderPass.Begin (cmd, fb); + cmd.SetViewport (dim, dim); + cmd.SetScissor (dim, dim); + pl.Bind (cmd); + cmd.Draw (3, 1, 0, 0); + pl.RenderPass.End (cmd); + cmd.End (); + + staggingQ.Submit (cmd); + staggingQ.WaitIdle (); + + cmd.Free (); + } + } + lutBrdf.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + } + + public enum CBTarget { IRRADIANCE = 0, PREFILTEREDENV = 1 }; + + public Image generateCubeMap (Queue staggingQ, CommandPool cmdPool, CBTarget target) { + const float deltaPhi = (2.0f * (float)Math.PI) / 180.0f; + const float deltaTheta = (0.5f * (float)Math.PI) / 64.0f; + + VkFormat format = VkFormat.R32g32b32a32Sfloat; + uint dim = 64; + + if (target == CBTarget.PREFILTEREDENV) { + format = VkFormat.R16g16b16a16Sfloat; + dim = 512; + } + + uint numMips = (uint)Math.Floor (Math.Log (dim, 2)) + 1; + + Image imgFbOffscreen = new Image (Dev, format, VkImageUsageFlags.TransferSrc | VkImageUsageFlags.ColorAttachment, + VkMemoryPropertyFlags.DeviceLocal, dim, dim); + imgFbOffscreen.SetName ("offscreenfb"); + imgFbOffscreen.CreateView (); + + Image cmap = new Image (Dev, format, VkImageUsageFlags.TransferDst | VkImageUsageFlags.Sampled, + VkMemoryPropertyFlags.DeviceLocal, dim, dim, VkImageType.Image2D, VkSampleCountFlags.SampleCount1, VkImageTiling.Optimal, + numMips, 6, 1, VkImageCreateFlags.CubeCompatible); + if (target == CBTarget.PREFILTEREDENV) + cmap.SetName ("prefilterenvmap"); + else + cmap.SetName ("irradianceCube"); + cmap.CreateView (VkImageViewType.Cube, VkImageAspectFlags.Color, 6, 0); + cmap.CreateSampler (VkSamplerAddressMode.ClampToEdge); + + DescriptorPool dsPool = new DescriptorPool (Dev, 2, new VkDescriptorPoolSize (VkDescriptorType.CombinedImageSampler)); + + DescriptorSetLayout dsLayout = new DescriptorSetLayout (Dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler)); + + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, VkSampleCountFlags.SampleCount1, false); + cfg.Layout = new PipelineLayout (Dev, dsLayout); + cfg.Layout.AddPushConstants ( + new VkPushConstantRange (VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, (uint)Marshal.SizeOf () + 8)); + + cfg.RenderPass = new RenderPass (Dev); + cfg.RenderPass.AddAttachment (format, VkImageLayout.ColorAttachmentOptimal); + cfg.RenderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0, 0, 0) }); + cfg.RenderPass.AddSubpass (new SubPass (VkImageLayout.ColorAttachmentOptimal)); + + cfg.AddVertexBinding (0, 3 * sizeof (float)); + cfg.AddVertexAttributes (0, VkFormat.R32g32b32Sfloat); + + cfg.AddShader (VkShaderStageFlags.Vertex, "#deferred.filtercube.vert.spv"); + if (target == CBTarget.PREFILTEREDENV) + cfg.AddShader (VkShaderStageFlags.Fragment, "#deferred.prefilterenvmap.frag.spv"); + else + cfg.AddShader (VkShaderStageFlags.Fragment, "#deferred.irradiancecube.frag.spv"); + + Matrix4x4[] matrices = { + // POSITIVE_X + Matrix4x4.CreateRotationX(Utils.DegreesToRadians(180)) * Matrix4x4.CreateRotationY(Utils.DegreesToRadians(90)), + // NEGATIVE_X + Matrix4x4.CreateRotationX(Utils.DegreesToRadians(180)) * Matrix4x4.CreateRotationY(Utils.DegreesToRadians(-90)), + // POSITIVE_Y + Matrix4x4.CreateRotationX(Utils.DegreesToRadians(-90)), + // NEGATIVE_Y + Matrix4x4.CreateRotationX(Utils.DegreesToRadians(90)), + // POSITIVE_Z + Matrix4x4.CreateRotationX(Utils.DegreesToRadians(180)), + // NEGATIVE_Z + Matrix4x4.CreateRotationZ(Utils.DegreesToRadians(180)) + }; + + VkImageSubresourceRange subRes = new VkImageSubresourceRange (VkImageAspectFlags.Color, 0, numMips, 0, 6); + + using (GraphicPipeline pl = new GraphicPipeline (cfg)) { + + DescriptorSet dset = dsPool.Allocate (dsLayout); + DescriptorSetWrites dsUpdate = new DescriptorSetWrites (dsLayout); + dsUpdate.Write (Dev, dset, cubemap.Descriptor); + Dev.WaitIdle (); + + using (Framebuffer fb = new Framebuffer (pl.RenderPass, dim, dim, imgFbOffscreen)) { + CommandBuffer cmd = cmdPool.AllocateCommandBuffer (); + cmd.Start (VkCommandBufferUsageFlags.OneTimeSubmit); + + cmap.SetLayout (cmd, VkImageLayout.Undefined, VkImageLayout.TransferDstOptimal, subRes); + + float roughness = 0; + + cmd.SetScissor (dim, dim); + cmd.SetViewport ((float)(dim), (float)dim); + + for (int m = 0; m < numMips; m++) { + roughness = (float)m / ((float)numMips - 1f); + + for (int f = 0; f < 6; f++) { + pl.RenderPass.Begin (cmd, fb); + + pl.Bind (cmd); + + float viewPortSize = (float)Math.Pow (0.5, m) * dim; + cmd.SetViewport (viewPortSize, viewPortSize); + cmd.PushConstant (pl.Layout, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, + matrices[f] * Matrix4x4.CreatePerspectiveFieldOfView (Utils.DegreesToRadians (90), 1f, 0.1f, 512f)); + if (target == CBTarget.IRRADIANCE) { + cmd.PushConstant (pl.Layout, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, deltaPhi, (uint)Marshal.SizeOf ()); + cmd.PushConstant (pl.Layout, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, deltaTheta, (uint)Marshal.SizeOf () + 4); + } else { + cmd.PushConstant (pl.Layout, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, roughness, (uint)Marshal.SizeOf ()); + cmd.PushConstant (pl.Layout, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, 64u, (uint)Marshal.SizeOf () + 4); + } + + cmd.BindDescriptorSet (pl.Layout, dset); + cmd.BindVertexBuffer (vboSkybox); + cmd.Draw (36); + + pl.RenderPass.End (cmd); + + imgFbOffscreen.SetLayout (cmd, VkImageAspectFlags.Color, + VkImageLayout.ColorAttachmentOptimal, VkImageLayout.TransferSrcOptimal); + + VkImageCopy region = new VkImageCopy (); + region.srcSubresource = new VkImageSubresourceLayers (VkImageAspectFlags.Color, 1); + region.dstSubresource = new VkImageSubresourceLayers (VkImageAspectFlags.Color, 1, (uint)m, (uint)f); + region.extent = new VkExtent3D { width = (uint)viewPortSize, height = (uint)viewPortSize, depth = 1 }; + + Vk.vkCmdCopyImage (cmd.Handle, + imgFbOffscreen.Handle, VkImageLayout.TransferSrcOptimal, + cmap.Handle, VkImageLayout.TransferDstOptimal, + 1, region.Pin ()); + region.Unpin (); + + imgFbOffscreen.SetLayout (cmd, VkImageAspectFlags.Color, + VkImageLayout.TransferSrcOptimal, VkImageLayout.ColorAttachmentOptimal); + + } + } + + cmap.SetLayout (cmd, VkImageLayout.TransferDstOptimal, VkImageLayout.ShaderReadOnlyOptimal, subRes); + + cmd.End (); + + staggingQ.Submit (cmd); + staggingQ.WaitIdle (); + + cmd.Free (); + } + } + cmap.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + + dsLayout.Dispose (); + imgFbOffscreen.Dispose (); + dsPool.Dispose (); + + return cmap; + } + + void generateCubemaps (Queue staggingQ, CommandPool cmdPool) { + irradianceCube = generateCubeMap (staggingQ, cmdPool, CBTarget.IRRADIANCE); + prefilterCube = generateCubeMap (staggingQ, cmdPool, CBTarget.PREFILTEREDENV); + } + + protected override void Dispose (bool disposing) { + vboSkybox.Dispose (); + cubemap.Dispose (); + lutBrdf.Dispose (); + irradianceCube.Dispose (); + prefilterCube.Dispose (); + + base.Dispose (disposing); + } + } + +} diff --git a/samples/deferred/deferred.csproj b/samples/deferred/deferred.csproj new file mode 100644 index 0000000..89a5f38 --- /dev/null +++ b/samples/deferred/deferred.csproj @@ -0,0 +1,43 @@ + + + + false + + + + + TRACE;DEBUG;NETSTANDARD;NETSTANDARD2_0;MEMORY_POOLS;WITH_SHADOWS;_WITH_VKVG + + + TRACE;DEBUG;NETSTANDARD;NETSTANDARD2_0;MEMORY_POOLS;WITH_SHADOWS;_WITH_VKVG + + + + + + + + + + + + deferred.%(Filename)%(Extension) + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/deferred/main-crow.cs b/samples/deferred/main-crow.cs new file mode 100644 index 0000000..fe0fc4c --- /dev/null +++ b/samples/deferred/main-crow.cs @@ -0,0 +1,404 @@ +using System; +using System.Numerics; +using Glfw; +using VK; +using CVKL; +using System.Collections.Generic; +using System.Linq; + +namespace deferred { + class Program : Crow.CrowWin { + static void Main (string[] args) { +#if DEBUG + Instance.VALIDATION = true; + Instance.DEBUG_UTILS = true; + Instance.RENDER_DOC_CAPTURE = false; +#endif + DeferredPbrRenderer.TEXTURE_ARRAY = true; + DeferredPbrRenderer.NUM_SAMPLES = VkSampleCountFlags.SampleCount1; + DeferredPbrRenderer.HDR_FORMAT = VkFormat.R32g32b32a32Sfloat; + DeferredPbrRenderer.MRT_FORMAT = VkFormat.R16g16b16a16Sfloat; + + PbrModelTexArray.TEXTURE_DIM = 1024; + + using (Program vke = new Program ()) { + vke.Run (); + } + } + + #region crow ui + public Crow.Command CMDViewScenes, CMDViewEditor, CMDViewDebug, CMDViewMaterials; + void init_crow_commands () { + CMDViewScenes = new Crow.Command (new Action (() => loadWindow ("#deferred.main.crow", this))) { Caption = "Lighting", Icon = new Crow.SvgPicture ("#deferred.crow.svg"), CanExecute = true }; + CMDViewEditor = new Crow.Command (new Action (() => loadWindow ("#deferred.scenes.crow", this))) { Caption = "Scenes", Icon = new Crow.SvgPicture ("#deferred.crow.svg"), CanExecute = true }; + CMDViewDebug = new Crow.Command (new Action (() => loadWindow ("#deferred.debug.crow", this))) { Caption = "Debug", Icon = new Crow.SvgPicture ("#deferred.crow.svg"), CanExecute = true }; + CMDViewMaterials = new Crow.Command (new Action (() => loadWindow ("#deferred.materials.crow", this))) { Caption = "Materials", Icon = new Crow.SvgPicture ("#deferred.crow.svg"), CanExecute = true }; + } + + public DeferredPbrRenderer.DebugView CurrentDebugView { + get { return renderer.currentDebugView; } + set { + if (value == renderer.currentDebugView) + return; + lock(crow.UpdateMutex) + renderer.currentDebugView = value; + rebuildBuffers = true; + NotifyValueChanged ("CurrentDebugView", renderer.currentDebugView); + } + } + + public float Gamma { + get { return renderer.matrices.gamma; } + set { + if (value == renderer.matrices.gamma) + return; + renderer.matrices.gamma = value; + NotifyValueChanged ("Gamma", value); + updateViewRequested = true; + } + } + public float Exposure { + get { return renderer.matrices.exposure; } + set { + if (value == renderer.matrices.exposure) + return; + renderer.matrices.exposure = value; + NotifyValueChanged ("Exposure", value); + updateViewRequested = true; + } + } + public float LightStrength { + get { return renderer.lights[renderer.lightNumDebug].color.X; } + set { + if (value == renderer.lights[renderer.lightNumDebug].color.X) + return; + renderer.lights[renderer.lightNumDebug].color = new Vector4(value); + NotifyValueChanged ("LightStrength", value); + renderer.uboLights.Update (renderer.lights); + } + } + public List Lights => renderer.lights.ToList (); + public List Scenes => renderer.model.Scenes; + #endregion + + public override string[] EnabledDeviceExtensions => new string[] { + Ext.D.VK_KHR_swapchain, + Ext.D.VK_EXT_debug_marker + }; + + protected override void configureEnabledFeatures (VkPhysicalDeviceFeatures available_features, ref VkPhysicalDeviceFeatures features) { + base.configureEnabledFeatures (available_features, ref features); + + features.samplerAnisotropy = available_features.samplerAnisotropy; + features.sampleRateShading = available_features.sampleRateShading; + features.geometryShader = available_features.geometryShader; + features.pipelineStatisticsQuery = true; + + if (available_features.textureCompressionETC2) { + features.textureCompressionETC2 = true; + Image.DefaultTextureFormat = VkFormat.Etc2R8g8b8a8UnormBlock; + }else if (available_features.textureCompressionBC) { + //features.textureCompressionBC = true; + //Image.DefaultTextureFormat = VkFormat.Bc3UnormBlock; + } + + + } + + protected override void createQueues () { + base.createQueues (); + transferQ = new Queue (dev, VkQueueFlags.Transfer); + } + + string[] cubemapPathes = { + Utils.DataDirectory + "textures/papermill.ktx", + Utils.DataDirectory + "textures/cubemap_yokohama_bc3_unorm.ktx", + Utils.DataDirectory + "textures/gcanyon_cube.ktx", + Utils.DataDirectory + "textures/pisa_cube.ktx", + Utils.DataDirectory + "textures/uffizi_cube.ktx", + }; + string[] modelPathes = { + //"/mnt/devel/gts/vkChess.net/data/models/chess.glb", + //"/home/jp/gltf/jaguar/scene.gltf", + Utils.DataDirectory + "models/DamagedHelmet/glTF/DamagedHelmet.gltf", + Utils.DataDirectory + "models/shadow.glb", + Utils.DataDirectory + "models/Hubble.glb", + Utils.DataDirectory + "models/MER_static.glb", + Utils.DataDirectory + "models/ISS_stationary.glb", + }; + + int curModelIndex = 0; + bool reloadModel; + bool rebuildBuffers; + + Queue transferQ; + DeferredPbrRenderer renderer; + PipelineStatisticsQueryPool statPool; + TimestampQueryPool timestampQPool; + ulong[] results; + + DebugReport dbgRepport; + + Program () : base() { + + if (Instance.DEBUG_UTILS) + dbgRepport = new DebugReport (instance, + VkDebugReportFlagsEXT.ErrorEXT + | VkDebugReportFlagsEXT.DebugEXT + | VkDebugReportFlagsEXT.WarningEXT + | VkDebugReportFlagsEXT.PerformanceWarningEXT + ); + + camera = new Camera (Utils.DegreesToRadians (45f), 1f, 0.1f, 16f); + camera.SetPosition (0, 0, 2); + + renderer = new DeferredPbrRenderer (dev, swapChain, presentQueue, cubemapPathes[2], camera.NearPlane, camera.FarPlane); + renderer.LoadModel (transferQ, modelPathes[curModelIndex]); + camera.Model = Matrix4x4.CreateScale (1f / Math.Max (Math.Max (renderer.modelAABB.Width, renderer.modelAABB.Height), renderer.modelAABB.Depth)); + + statPool = new PipelineStatisticsQueryPool (dev, + VkQueryPipelineStatisticFlags.InputAssemblyVertices | + VkQueryPipelineStatisticFlags.InputAssemblyPrimitives | + VkQueryPipelineStatisticFlags.ClippingInvocations | + VkQueryPipelineStatisticFlags.ClippingPrimitives | + VkQueryPipelineStatisticFlags.FragmentShaderInvocations); + + timestampQPool = new TimestampQueryPool (dev); + + init_crow_commands (); + + crow.Load ("#deferred.menu.crow").DataSource = this; + + } + + protected override void recordDraw (CommandBuffer cmd, int imageIndex) { + statPool.Begin (cmd); + renderer.buildCommandBuffers (cmd, imageIndex); + statPool.End (cmd); + } + + public override void UpdateView () { + renderer.UpdateView (camera); + updateViewRequested = false; +#if WITH_SHADOWS + if (renderer.shadowMapRenderer.updateShadowMap) + renderer.shadowMapRenderer.update_shadow_map (cmdPool); +#endif + } + + int frameCount = 0; + public override void Update () { + if (reloadModel) { + renderer.LoadModel (transferQ, modelPathes[curModelIndex]); + reloadModel = false; + camera.Model = Matrix4x4.CreateScale (1f / Math.Max (Math.Max (renderer.modelAABB.Width, renderer.modelAABB.Height), renderer.modelAABB.Depth)); + updateViewRequested = true; + rebuildBuffers = true; +#if WITH_SHADOWS + renderer.shadowMapRenderer.updateShadowMap = true; +#endif + } + + base.Update (); + + if (++frameCount > 20) { + NotifyValueChanged ("fps", fps); + frameCount = 0; + } + + results = statPool.GetResults (); + } + protected override void OnResize () { + renderer.Resize (); + base.OnResize (); + } + + #region Mouse and keyboard + protected override void onMouseMove (double xPos, double yPos) { + if (crow.ProcessMouseMove ((int)xPos, (int)yPos)) + return; + + double diffX = lastMouseX - xPos; + double diffY = lastMouseY - yPos; + if (MouseButton[0]) { + camera.Rotate ((float)-diffX, (float)-diffY); + } else if (MouseButton[1]) { + camera.SetZoom ((float)diffY); + } else + return; + + updateViewRequested = true; + } + protected override void onMouseButtonDown (Glfw.MouseButton button) { + if (crow.ProcessMouseButtonDown ((Crow.MouseButton)button)) + return; + base.onMouseButtonDown (button); + } + protected override void onMouseButtonUp (Glfw.MouseButton button) { + if (crow.ProcessMouseButtonUp ((Crow.MouseButton)button)) + return; + base.onMouseButtonUp (button); + } + protected override void onKeyDown (Key key, int scanCode, Modifier modifiers) { + if (crow.ProcessKeyDown ((Crow.Key)key)) + return; + switch (key) { + case Key.F: + if (modifiers.HasFlag (Modifier.Shift)) { + renderer.debugFace--; + if (renderer.debugFace < 0) + renderer.debugFace = 5; + } else { + renderer.debugFace++; + if (renderer.debugFace >= 5) + renderer.debugFace = 0; + } + rebuildBuffers = true; + break; + case Key.M: + if (modifiers.HasFlag (Modifier.Shift)) { + renderer.debugMip--; + if (renderer.debugMip < 0) + renderer.debugMip = (int)renderer.envCube.prefilterCube.CreateInfo.mipLevels - 1; + } else { + renderer.debugMip++; + if (renderer.debugMip >= renderer.envCube.prefilterCube.CreateInfo.mipLevels) + renderer.debugMip = 0; + } + rebuildBuffers = true; + break; + case Key.L: + if (modifiers.HasFlag (Modifier.Shift)) { + renderer.lightNumDebug--; + if (renderer.lightNumDebug < 0) + renderer.lightNumDebug = (int)renderer.lights.Length - 1; + } else { + renderer.lightNumDebug++; + if (renderer.lightNumDebug >= renderer.lights.Length) + renderer.lightNumDebug = 0; + } + rebuildBuffers = true; + break; + case Key.Keypad0: + case Key.Keypad1: + case Key.Keypad2: + case Key.Keypad3: + case Key.Keypad4: + case Key.Keypad5: + case Key.Keypad6: + case Key.Keypad7: + case Key.Keypad8: + case Key.Keypad9: + renderer.currentDebugView = (DeferredPbrRenderer.DebugView)(int)key-320; + rebuildBuffers = true; + break; + case Key.KeypadDivide: + renderer.currentDebugView = DeferredPbrRenderer.DebugView.irradiance; + rebuildBuffers = true; + break; + case Key.S: + if (modifiers.HasFlag (Modifier.Control)) { + renderer.pipelineCache.Save (); + Console.WriteLine ($"Pipeline Cache saved."); + } else { + renderer.currentDebugView = DeferredPbrRenderer.DebugView.shadowMap; + rebuildBuffers = true; + } + break; + case Key.Up: + if (modifiers.HasFlag (Modifier.Shift)) + renderer.MoveLight(-Vector4.UnitZ); + else + camera.Move (0, 0, 1); + break; + case Key.Down: + if (modifiers.HasFlag (Modifier.Shift)) + renderer.MoveLight (Vector4.UnitZ); + else + camera.Move (0, 0, -1); + break; + case Key.Left: + if (modifiers.HasFlag (Modifier.Shift)) + renderer.MoveLight (-Vector4.UnitX); + else + camera.Move (1, 0, 0); + break; + case Key.Right: + if (modifiers.HasFlag (Modifier.Shift)) + renderer.MoveLight (Vector4.UnitX); + else + camera.Move (-1, 0, 0); + break; + case Key.PageUp: + if (modifiers.HasFlag (Modifier.Shift)) + renderer.MoveLight (Vector4.UnitY); + else + camera.Move (0, 1, 0); + break; + case Key.PageDown: + if (modifiers.HasFlag (Modifier.Shift)) + renderer.MoveLight (-Vector4.UnitY); + else + camera.Move (0, -1, 0); + break; + case Key.F2: + if (modifiers.HasFlag (Modifier.Shift)) + renderer.matrices.exposure -= 0.3f; + else + renderer.matrices.exposure += 0.3f; + break; + case Key.F3: + if (modifiers.HasFlag (Modifier.Shift)) + renderer.matrices.gamma -= 0.1f; + else + renderer.matrices.gamma += 0.1f; + break; + case Key.F4: + if (camera.Type == Camera.CamType.FirstPerson) + camera.Type = Camera.CamType.LookAt; + else + camera.Type = Camera.CamType.FirstPerson; + Console.WriteLine ($"camera type = {camera.Type}"); + break; + case Key.KeypadAdd: + curModelIndex++; + if (curModelIndex >= modelPathes.Length) + curModelIndex = 0; + reloadModel = true; + break; + case Key.KeypadSubtract: + curModelIndex--; + if (curModelIndex < 0) + curModelIndex = modelPathes.Length -1; + reloadModel = true; + break; + default: + base.onKeyDown (key, scanCode, modifiers); + return; + } + updateViewRequested = true; + } + protected override void onKeyUp (Key key, int scanCode, Modifier modifiers) { + if (crow.ProcessKeyUp ((Crow.Key)key)) + return; + } + protected override void onChar (CodePoint cp) { + if (crow.ProcessKeyPress (cp.ToChar ())) + return; + } + #endregion + + protected override void Dispose (bool disposing) { + if (disposing) { + if (!isDisposed) { + renderer.Dispose (); + statPool.Dispose (); + timestampQPool.Dispose (); + dbgRepport?.Dispose (); + } + } + + base.Dispose (disposing); + } + } +} diff --git a/samples/deferred/main.cs b/samples/deferred/main.cs new file mode 100644 index 0000000..f15132b --- /dev/null +++ b/samples/deferred/main.cs @@ -0,0 +1,549 @@ +using System; +using System.Numerics; +using System.Runtime.InteropServices; +using CVKL; +using CVKL.glTF; +using Glfw; +using VK; + +namespace deferred { + class Deferred : VkWindow { + static void Main (string[] args) { +#if DEBUG + Instance.VALIDATION = true; + Instance.DEBUG_UTILS = true; + Instance.RENDER_DOC_CAPTURE = false; +#endif + SwapChain.PREFERED_FORMAT = VkFormat.B8g8r8a8Srgb; + DeferredPbrRenderer.TEXTURE_ARRAY = true; + DeferredPbrRenderer.NUM_SAMPLES = VkSampleCountFlags.SampleCount1; + DeferredPbrRenderer.HDR_FORMAT = VkFormat.R16g16b16a16Sfloat; + DeferredPbrRenderer.MRT_FORMAT = VkFormat.R32g32b32a32Sfloat; + + PbrModelTexArray.TEXTURE_DIM = 1024; + + using (Deferred vke = new Deferred ()) { + vke.Run (); + } + } + + public override string[] EnabledDeviceExtensions => new string[] { + Ext.D.VK_KHR_swapchain, + Ext.D.VK_EXT_debug_marker + }; + + protected override void configureEnabledFeatures (VkPhysicalDeviceFeatures available_features, ref VkPhysicalDeviceFeatures enabled_features) { + base.configureEnabledFeatures (available_features, ref enabled_features); + + enabled_features.samplerAnisotropy = available_features.samplerAnisotropy; + enabled_features.sampleRateShading = available_features.sampleRateShading; + enabled_features.geometryShader = available_features.geometryShader; + + enabled_features.textureCompressionBC = available_features.textureCompressionBC; + } + + protected override void createQueues () { + base.createQueues (); + transferQ = new Queue (dev, VkQueueFlags.Transfer); + computeQ = new Queue (dev, VkQueueFlags.Compute); + } + string[] cubemapPathes = { + Utils.DataDirectory + "textures/papermill.ktx", + Utils.DataDirectory + "textures/cubemap_yokohama_bc3_unorm.ktx", + Utils.DataDirectory + "textures/gcanyon_cube.ktx", + Utils.DataDirectory + "textures/pisa_cube.ktx", + Utils.DataDirectory + "textures/uffizi_cube.ktx", + }; + string[] modelPathes = { + //"/mnt/devel/gts/vkChess.net/data/models/chess.glb", + Utils.DataDirectory + "models/DamagedHelmet/glTF/DamagedHelmet.gltf", + Utils.DataDirectory + "models/shadow.glb", + Utils.DataDirectory + "models/Hubble.glb", + Utils.DataDirectory + "models/MER_static.glb", + Utils.DataDirectory + "models/ISS_stationary.glb", + }; + + int curModelIndex = 0; + bool reloadModel; + bool rebuildBuffers; + + Queue transferQ, computeQ; + DeferredPbrRenderer renderer; + + + GraphicPipeline plToneMap; + Framebuffer[] frameBuffers; + DescriptorPool descriptorPool; + DescriptorSet descriptorSet; + + + + Deferred () : base("deferred") { + camera = new Camera (Utils.DegreesToRadians (45f), 1f, 0.1f, 16f); + camera.SetPosition (0, 0, 2); + + //renderer = new DeferredPbrRenderer (presentQueue, cubemapPathes[2], swapChain.Width, swapChain.Height, camera.NearPlane, camera.FarPlane); + renderer = new DeferredPbrRenderer (presentQueue, cubemapPathes[2], swapChain.Width, swapChain.Height, camera.NearPlane, camera.FarPlane); + renderer.LoadModel (transferQ, modelPathes[curModelIndex]); + camera.Model = Matrix4x4.CreateScale (1f / Math.Max (Math.Max (renderer.modelAABB.Width, renderer.modelAABB.Height), renderer.modelAABB.Depth)); + + init_final_pl (); + } + + void init_final_pl() { + descriptorPool = new DescriptorPool (dev, 3, + new VkDescriptorPoolSize (VkDescriptorType.CombinedImageSampler, 2), + new VkDescriptorPoolSize (VkDescriptorType.StorageImage, 4) + ); + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, DeferredPbrRenderer.NUM_SAMPLES); + + cfg.Layout = new PipelineLayout (dev, + new VkPushConstantRange (VkShaderStageFlags.Fragment, 2 * sizeof (float)), + new DescriptorSetLayout (dev, 0, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler) + )); + + cfg.RenderPass = new RenderPass (dev, swapChain.ColorFormat, DeferredPbrRenderer.NUM_SAMPLES); + + cfg.AddShader (VkShaderStageFlags.Vertex, "#deferred.FullScreenQuad.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "#deferred.tone_mapping.frag.spv"); + + plToneMap = new GraphicPipeline (cfg); + + descriptorSet = descriptorPool.Allocate (cfg.Layout.DescriptorSetLayouts[0]); + + init_blur (); + } + + ComputePipeline plBlur; + DescriptorSetLayout dsLayoutBlur; + DescriptorSet dsetBlurPing, dsetBlurPong; + Image downSamp, downSamp2; + CommandPool computeCmdPool; + + struct BlurPushCsts { + public Vector2 texSize; + public int dir; + public float scale; + public float strength; + }; + BlurPushCsts pcBloom = new BlurPushCsts () { strength = 1.3f, scale = 0.4f }; + + void init_blur () { + computeCmdPool = new CommandPool (computeQ); + + blurComplete = dev.CreateSemaphore (); + + dsLayoutBlur = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Compute, VkDescriptorType.StorageImage), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Compute, VkDescriptorType.StorageImage) + ); + plBlur = new ComputePipeline ( + new PipelineLayout (dev, new VkPushConstantRange (VkShaderStageFlags.Compute, (uint)Marshal.SizeOf ()), dsLayoutBlur), + "#deferred.bloom.comp.spv"); + + dsetBlurPing = descriptorPool.Allocate (dsLayoutBlur); + dsetBlurPong = descriptorPool.Allocate (dsLayoutBlur); + } + + void buildBlurCmd (CommandBuffer cmd) { + renderer.hdrImgResolved.SetLayout (cmd, VkImageAspectFlags.Color, + VkAccessFlags.ColorAttachmentWrite, VkAccessFlags.TransferRead, + VkImageLayout.ColorAttachmentOptimal, VkImageLayout.TransferSrcOptimal, + VkPipelineStageFlags.ColorAttachmentOutput, VkPipelineStageFlags.Transfer); + downSamp.SetLayout (cmd, VkImageAspectFlags.Color, + VkAccessFlags.ShaderRead, VkAccessFlags.TransferWrite, + VkImageLayout.ShaderReadOnlyOptimal, VkImageLayout.TransferDstOptimal, + VkPipelineStageFlags.FragmentShader, VkPipelineStageFlags.Transfer); + + + renderer.hdrImgResolved.BlitTo (cmd, downSamp); + + renderer.hdrImgResolved.SetLayout (cmd, VkImageAspectFlags.Color, + VkImageLayout.TransferSrcOptimal, VkImageLayout.ShaderReadOnlyOptimal, + VkPipelineStageFlags.Transfer, VkPipelineStageFlags.FragmentShader); + + downSamp2.SetLayout (cmd, VkImageAspectFlags.Color, + 0, VkAccessFlags.MemoryWrite, + VkImageLayout.Undefined, VkImageLayout.General, + VkPipelineStageFlags.AllCommands, VkPipelineStageFlags.ComputeShader); + + downSamp.SetLayout (cmd, VkImageAspectFlags.Color, + VkAccessFlags.TransferWrite, VkAccessFlags.MemoryRead, + VkImageLayout.TransferDstOptimal, VkImageLayout.General, + VkPipelineStageFlags.Transfer, VkPipelineStageFlags.ComputeShader); + + plBlur.Bind (cmd); + + pcBloom.dir = 0; + /* + plBlur.BindDescriptorSet (cmd, dsetBlurPing); + cmd.PushConstant (plBlur.Layout, VkShaderStageFlags.Compute, pcBloom); + cmd.Dispatch (downSamp.Width / 16, downSamp.Height / 16); + + cmd.SetMemoryBarrier (VkPipelineStageFlags.ComputeShader, VkPipelineStageFlags.ComputeShader, + VkAccessFlags.ShaderWrite, VkAccessFlags.ShaderRead); + + plBlur.BindDescriptorSet (cmd, dsetBlurPong); + cmd.PushConstant (plBlur.Layout, VkShaderStageFlags.Compute, 1, (uint)Marshal.SizeOf ()); + cmd.Dispatch (downSamp.Width / 16, downSamp.Height / 16); + + downSamp.SetLayout (cmd, VkImageAspectFlags.Color, + VkAccessFlags.MemoryWrite, VkAccessFlags.ShaderRead, + VkImageLayout.General, VkImageLayout.ShaderReadOnlyOptimal, + VkPipelineStageFlags.ComputeShader, VkPipelineStageFlags.FragmentShader);*/ + + downSamp.SetLayout (cmd, VkImageAspectFlags.Color, + VkAccessFlags.TransferWrite, VkAccessFlags.ShaderRead, + VkImageLayout.TransferDstOptimal, VkImageLayout.ShaderReadOnlyOptimal, + VkPipelineStageFlags.Transfer, VkPipelineStageFlags.FragmentShader); + + cmd.End (); + } + + + CommandBuffer cmdPbr; + CommandBuffer cmdBlur; + VkSemaphore blurComplete; + const uint downSizing = 1; + float finalDebug = -1.0f; + + void buildCommandBuffers () { + cmdPbr?.Free (); + cmdPbr = cmdPool.AllocateAndStart (); + renderer.buildCommandBuffers (cmdPbr); + cmdPbr.End (); + + cmdBlur?.Free (); + cmdBlur = computeCmdPool.AllocateAndStart (); + buildBlurCmd (cmdBlur); + + + for (int i = 0; i < swapChain.ImageCount; ++i) { + cmds[i]?.Free (); + cmds[i] = cmdPool.AllocateAndStart (); + + //renderer.hdrImgResolved.SetLayout (cmds[i], VkImageAspectFlags.Color, + //VkAccessFlags.TransferRead, VkAccessFlags.ShaderRead, + //VkImageLayout.TransferSrcOptimal, VkImageLayout.ShaderReadOnlyOptimal, + //VkPipelineStageFlags.Transfer, VkPipelineStageFlags.FragmentShader); + + plToneMap.RenderPass.Begin (cmds[i], frameBuffers[i]); + + cmds[i].SetViewport (frameBuffers[i].Width, frameBuffers[i].Height); + cmds[i].SetScissor (frameBuffers[i].Width, frameBuffers[i].Height); + + plToneMap.Bind (cmds[i]); + plToneMap.BindDescriptorSet (cmds[i], descriptorSet); + + cmds[i].PushConstant (plToneMap.Layout, VkShaderStageFlags.Fragment, 12, new float[] { renderer.exposure, renderer.gamma, finalDebug }, 0); + + cmds[i].Draw (3, 1, 0, 0); + + plToneMap.RenderPass.End (cmds[i]); + + renderer.hdrImgResolved.SetLayout (cmds[i], VkImageAspectFlags.Color, + VkAccessFlags.ShaderRead, VkAccessFlags.ColorAttachmentWrite, + VkImageLayout.ShaderReadOnlyOptimal, VkImageLayout.ColorAttachmentOptimal, + VkPipelineStageFlags.FragmentShader, VkPipelineStageFlags.ColorAttachmentOutput); + + cmds[i].End (); + } + } + + public override void UpdateView () { + renderer.UpdateView (camera); + updateViewRequested = false; +#if WITH_SHADOWS + if (renderer.shadowMapRenderer.updateShadowMap) + renderer.shadowMapRenderer.update_shadow_map (cmdPool); +#endif + } + + public override void Update () { + if (reloadModel) { + renderer.LoadModel (transferQ, modelPathes[curModelIndex]); + reloadModel = false; + camera.Model = Matrix4x4.CreateScale (1f / Math.Max (Math.Max (renderer.modelAABB.Width, renderer.modelAABB.Height), renderer.modelAABB.Depth)); + updateViewRequested = true; + rebuildBuffers = true; +#if WITH_SHADOWS + renderer.shadowMapRenderer.updateShadowMap = true; +#endif + } + + if (rebuildBuffers) { + buildCommandBuffers (); + rebuildBuffers = false; + } + + } + + + protected override void render () { + int idx = swapChain.GetNextImage (); + if (idx < 0) { + OnResize (); + return; + } + + if (cmds[idx] == null) + return; + + presentQueue.Submit (cmdPbr, swapChain.presentComplete, renderer.DrawComplete); + + computeQ.Submit (cmdBlur, renderer.DrawComplete, blurComplete); + + presentQueue.Submit (cmds[idx], blurComplete, drawComplete[idx]); + presentQueue.Present (swapChain, drawComplete[idx]); + + presentQueue.WaitIdle (); + } + protected override void OnResize () { + dev.WaitIdle (); + + renderer.Resize (swapChain.Width, swapChain.Height); + + UpdateView (); + + downSamp?.Dispose (); + downSamp2?.Dispose (); + downSamp = new Image (dev, VkFormat.R16g16b16a16Sfloat, VkImageUsageFlags.TransferDst | VkImageUsageFlags.Storage | VkImageUsageFlags.Sampled, + VkMemoryPropertyFlags.DeviceLocal, renderer.Width / downSizing, renderer.Height / downSizing, VkImageType.Image2D); + downSamp2 = new Image (dev, VkFormat.R16g16b16a16Sfloat, VkImageUsageFlags.Storage, + VkMemoryPropertyFlags.DeviceLocal, renderer.Width / downSizing, renderer.Height/ downSizing, VkImageType.Image2D); + downSamp.CreateView (); downSamp.CreateSampler (); + downSamp.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + downSamp2.CreateView (); downSamp2.CreateSampler (); + downSamp2.Descriptor.imageLayout = VkImageLayout.General; + + downSamp.SetName ("HDRimgDownScaled"); + downSamp2.SetName ("HDRimgDownScaled2"); + + pcBloom.texSize.X = downSamp.Width; + pcBloom.texSize.Y = downSamp.Height; + + if (frameBuffers != null) + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); + + frameBuffers = new Framebuffer[swapChain.ImageCount]; + + for (int i = 0; i < swapChain.ImageCount; ++i) { + frameBuffers[i] = new Framebuffer (plToneMap.RenderPass, swapChain.Width, swapChain.Height, + (plToneMap.Samples == VkSampleCountFlags.SampleCount1) ? new Image[] { + swapChain.images[i], + } : new Image[] { + null, + swapChain.images[i] + }); + } + + DescriptorSetWrites dsUpdate = new DescriptorSetWrites (plToneMap.Layout.DescriptorSetLayouts[0]); + dsUpdate.Write (dev, descriptorSet, renderer.hdrImgResolved.Descriptor, downSamp.Descriptor); + + dsUpdate = new DescriptorSetWrites (dsLayoutBlur); + downSamp.Descriptor.imageLayout = VkImageLayout.General; + dsUpdate.Write (dev, dsetBlurPong, downSamp2.Descriptor, downSamp.Descriptor); + dsUpdate.Write (dev, dsetBlurPing, downSamp.Descriptor, downSamp2.Descriptor); + + buildCommandBuffers (); + + dev.WaitIdle (); + } + + #region Mouse and keyboard + protected override void onScroll (double xOffset, double yOffset) { + } + protected override void onMouseMove (double xPos, double yPos) { + double diffX = lastMouseX - xPos; + double diffY = lastMouseY - yPos; + if (MouseButton[0]) { + camera.Rotate ((float)-diffX, (float)-diffY); + } else if (MouseButton[1]) { + camera.SetZoom ((float)diffY); + } else + return; + + updateViewRequested = true; + } + protected override void onKeyDown (Key key, int scanCode, Modifier modifiers) { + switch (key) { + case Key.F: + if (modifiers.HasFlag (Modifier.Shift)) { + renderer.debugFace--; + if (renderer.debugFace < 0) + renderer.debugFace = 5; + } else { + renderer.debugFace++; + if (renderer.debugFace >= 5) + renderer.debugFace = 0; + } + rebuildBuffers = true; + break; + case Key.M: + if (modifiers.HasFlag (Modifier.Shift)) { + renderer.debugMip--; + if (renderer.debugMip < 0) + renderer.debugMip = (int)renderer.envCube.prefilterCube.CreateInfo.mipLevels - 1; + } else { + renderer.debugMip++; + if (renderer.debugMip >= renderer.envCube.prefilterCube.CreateInfo.mipLevels) + renderer.debugMip = 0; + } + rebuildBuffers = true; + break; + case Key.L: + if (modifiers.HasFlag (Modifier.Shift)) { + renderer.lightNumDebug--; + if (renderer.lightNumDebug < 0) + renderer.lightNumDebug = (int)renderer.lights.Length - 1; + } else { + renderer.lightNumDebug++; + if (renderer.lightNumDebug >= renderer.lights.Length) + renderer.lightNumDebug = 0; + } + rebuildBuffers = true; + break; + case Key.Keypad0: + case Key.Keypad1: + case Key.Keypad2: + case Key.Keypad3: + case Key.Keypad4: + case Key.Keypad5: + case Key.Keypad6: + case Key.Keypad7: + case Key.Keypad8: + case Key.Keypad9: + renderer.currentDebugView = (DeferredPbrRenderer.DebugView)(int)key-320; + rebuildBuffers = true; + break; + case Key.KeypadDivide: + renderer.currentDebugView = DeferredPbrRenderer.DebugView.irradiance; + rebuildBuffers = true; + break; + case Key.S: + if (modifiers.HasFlag (Modifier.Control)) { + renderer.pipelineCache.Save (); + Console.WriteLine ($"Pipeline Cache saved."); + } else { + renderer.currentDebugView = DeferredPbrRenderer.DebugView.shadowMap; + rebuildBuffers = true; + } + break; + case Key.Up: + if (modifiers.HasFlag (Modifier.Shift)) + renderer.MoveLight(-Vector4.UnitZ); + else + camera.Move (0, 0, 1); + break; + case Key.Down: + if (modifiers.HasFlag (Modifier.Shift)) + renderer.MoveLight (Vector4.UnitZ); + else + camera.Move (0, 0, -1); + break; + case Key.Left: + if (modifiers.HasFlag (Modifier.Shift)) + renderer.MoveLight (-Vector4.UnitX); + else + camera.Move (1, 0, 0); + break; + case Key.Right: + if (modifiers.HasFlag (Modifier.Shift)) + renderer.MoveLight (Vector4.UnitX); + else + camera.Move (-1, 0, 0); + break; + case Key.PageUp: + if (modifiers.HasFlag (Modifier.Shift)) + renderer.MoveLight (Vector4.UnitY); + else + camera.Move (0, 1, 0); + break; + case Key.PageDown: + if (modifiers.HasFlag (Modifier.Shift)) + renderer.MoveLight (-Vector4.UnitY); + else + camera.Move (0, -1, 0); + break; + case Key.F2: + if (modifiers.HasFlag (Modifier.Shift)) + renderer.exposure -= 0.3f; + else + renderer.exposure += 0.3f; + rebuildBuffers = true; + break; + case Key.F3: + if (modifiers.HasFlag (Modifier.Shift)) + renderer.gamma -= 0.1f; + else + renderer.gamma += 0.1f; + rebuildBuffers = true; + break; + case Key.D: + finalDebug = -finalDebug; + rebuildBuffers = true; + break; + case Key.B: + if (modifiers.HasFlag (Modifier.Control)) { + if (modifiers.HasFlag (Modifier.Shift)) + pcBloom.strength -= 0.1f; + else + pcBloom.strength += 0.1f; + } else { + if (modifiers.HasFlag (Modifier.Shift)) + pcBloom.scale *= 1.1f; + else + pcBloom.scale *= 0.9f; + } + Console.WriteLine ($"Bloom: scale = {pcBloom.scale}, strength = {pcBloom.strength}"); + rebuildBuffers = true; + //if (camera.Type == Camera.CamType.FirstPerson) + // camera.Type = Camera.CamType.LookAt; + //else + // camera.Type = Camera.CamType.FirstPerson; + //Console.WriteLine ($"camera type = {camera.Type}"); + break; + case Key.KeypadAdd: + curModelIndex++; + if (curModelIndex >= modelPathes.Length) + curModelIndex = 0; + reloadModel = true; + break; + case Key.KeypadSubtract: + curModelIndex--; + if (curModelIndex < 0) + curModelIndex = modelPathes.Length -1; + reloadModel = true; + break; + default: + base.onKeyDown (key, scanCode, modifiers); + return; + } + updateViewRequested = true; + } + #endregion + + protected override void Dispose (bool disposing) { + if (disposing) { + if (!isDisposed) { + computeCmdPool.Dispose (); + downSamp?.Dispose (); + downSamp2?.Dispose (); + if (frameBuffers != null) + foreach (Framebuffer fb in frameBuffers) + fb.Dispose (); + renderer.Dispose (); + plBlur.Dispose (); + plToneMap.Dispose (); + descriptorPool.Dispose (); + } + dev.DestroySemaphore (blurComplete); + } + base.Dispose (disposing); + } + } +} diff --git a/samples/deferred/mainShadow.cs b/samples/deferred/mainShadow.cs new file mode 100644 index 0000000..0bc3103 --- /dev/null +++ b/samples/deferred/mainShadow.cs @@ -0,0 +1,415 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.InteropServices; +using Glfw; +using VK; +using CVKL; + +namespace deferredShadow { + class Program : VkWindow{ + static void Main (string[] args) { + using (Program vke = new Program ()) { + vke.Run (); + } + } + + VkSampleCountFlags samples = VkSampleCountFlags.SampleCount1; + + public struct Matrices { + public Matrix4x4 projection; + public Matrix4x4 view; + public Matrix4x4 model; + public Vector4 lightPos; + public float gamma; + public float exposure; + } + + public Matrices matrices = new Matrices { + lightPos = new Vector4 (1.0f, 0.0f, 0.0f, 1.0f), + gamma = 1.0f, + exposure = 2.0f, + }; + + const uint SHADOW_MAP_SIZE = 4096; + +#if DEBUG + PipelineStatisticsQueryPool statPool; + TimestampQueryPool timestampQPool; + ulong[] results; +#endif + + protected override void configureEnabledFeatures (ref VkPhysicalDeviceFeatures features) { + base.configureEnabledFeatures (ref features); +#if DEBUG + features.pipelineStatisticsQuery = true; +#endif + } + + Program () : base(true) { + //camera.Model = Matrix4x4.CreateRotationX (Utils.DegreesToRadians (-90)) * Matrix4x4.CreateRotationY (Utils.DegreesToRadians (180)); + //camera.SetRotation (-0.1f,-0.4f); + camera.SetPosition (0, 0, -3); + + init (); + +#if DEBUG + statPool = new PipelineStatisticsQueryPool (dev, + VkQueryPipelineStatisticFlags.InputAssemblyVertices | + VkQueryPipelineStatisticFlags.InputAssemblyPrimitives | + VkQueryPipelineStatisticFlags.ClippingInvocations | + VkQueryPipelineStatisticFlags.ClippingPrimitives | + VkQueryPipelineStatisticFlags.FragmentShaderInvocations); + + timestampQPool = new TimestampQueryPool (dev); +#endif + } + + Framebuffer[] frameBuffers; + Image gbColorRough, gbEmitMetal, gbN, gbPos; + + DescriptorPool descriptorPool; + DescriptorSetLayout descLayoutMain, descLayoutModelTextures, descLayoutGBuff; + DescriptorSet dsMain, dsGBuff; + + Pipeline gBuffPipeline, composePipeline; + + HostBuffer uboMats; + + RenderPass renderPass; + + Model model; + EnvironmentCube envCube; + + DebugDrawPipeline debugDraw; + Framebuffer[] debugFB; + + void init () { + VkFormat depthFormat = dev.GetSuitableDepthFormat (); + renderPass = new RenderPass (dev); + + renderPass.AddAttachment (swapChain.ColorFormat, VkImageLayout.ColorAttachmentOptimal, VkSampleCountFlags.SampleCount1); + renderPass.AddAttachment (depthFormat, VkImageLayout.DepthStencilAttachmentOptimal, samples); + renderPass.AddAttachment (VkFormat.R8g8b8a8Unorm, VkImageLayout.ColorAttachmentOptimal); + renderPass.AddAttachment (VkFormat.R8g8b8a8Unorm, VkImageLayout.ColorAttachmentOptimal); + renderPass.AddAttachment (VkFormat.R16g16b16a16Sfloat, VkImageLayout.ColorAttachmentOptimal); + renderPass.AddAttachment (VkFormat.R16g16b16a16Sfloat, VkImageLayout.ColorAttachmentOptimal); + //renderPass.AddAttachment (VkFormat.R16g16b16a16Sfloat, VkImageLayout.DepthStencilReadOnlyOptimal); + + renderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0.0f, 0.0f, 0.0f) }); + renderPass.ClearValues.Add (new VkClearValue { depthStencil = new VkClearDepthStencilValue (1.0f, 0) }); + renderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0.0f, 0.0f, 0.0f) }); + renderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0.0f, 0.0f, 0.0f) }); + renderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0.0f, 0.0f, 0.0f) }); + renderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0.0f, 0.0f, 0.0f) }); + + SubPass[] subpass = { new SubPass (), new SubPass (), new SubPass() }; + subpass[0].AddColorReference ( new VkAttachmentReference (2, VkImageLayout.ColorAttachmentOptimal), + new VkAttachmentReference (3, VkImageLayout.ColorAttachmentOptimal), + new VkAttachmentReference (4, VkImageLayout.ColorAttachmentOptimal), + new VkAttachmentReference (5, VkImageLayout.ColorAttachmentOptimal)); + subpass[0].SetDepthReference (1, VkImageLayout.DepthStencilAttachmentOptimal); + + subpass[1].AddColorReference (0, VkImageLayout.ColorAttachmentOptimal); + subpass[1].AddInputReference ( new VkAttachmentReference (2, VkImageLayout.ShaderReadOnlyOptimal), + new VkAttachmentReference (3, VkImageLayout.ShaderReadOnlyOptimal), + new VkAttachmentReference (4, VkImageLayout.ShaderReadOnlyOptimal), + new VkAttachmentReference (5, VkImageLayout.ShaderReadOnlyOptimal)); + renderPass.AddSubpass (subpass); + + renderPass.AddDependency (Vk.SubpassExternal, 0, + VkPipelineStageFlags.BottomOfPipe, VkPipelineStageFlags.ColorAttachmentOutput, + VkAccessFlags.MemoryRead, VkAccessFlags.ColorAttachmentRead | VkAccessFlags.ColorAttachmentWrite); + renderPass.AddDependency (0, 1, + VkPipelineStageFlags.ColorAttachmentOutput, VkPipelineStageFlags.FragmentShader, + VkAccessFlags.ColorAttachmentWrite, VkAccessFlags.ShaderRead); + renderPass.AddDependency (1, Vk.SubpassExternal, + VkPipelineStageFlags.ColorAttachmentOutput, VkPipelineStageFlags.BottomOfPipe, + VkAccessFlags.ColorAttachmentRead | VkAccessFlags.ColorAttachmentWrite, VkAccessFlags.MemoryRead); + + + descriptorPool = new DescriptorPool (dev, 3, + new VkDescriptorPoolSize (VkDescriptorType.UniformBuffer, 2), + new VkDescriptorPoolSize (VkDescriptorType.CombinedImageSampler, 3), + new VkDescriptorPoolSize (VkDescriptorType.InputAttachment, 4) + ); + + descLayoutMain = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, VkDescriptorType.UniformBuffer), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (2, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (3, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler)); + + descLayoutModelTextures = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (2, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (3, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (4, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler) + ); + + descLayoutGBuff = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Fragment, VkDescriptorType.InputAttachment), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Fragment, VkDescriptorType.InputAttachment), + new VkDescriptorSetLayoutBinding (2, VkShaderStageFlags.Fragment, VkDescriptorType.InputAttachment), + new VkDescriptorSetLayoutBinding (3, VkShaderStageFlags.Fragment, VkDescriptorType.InputAttachment)); + + dsMain = descriptorPool.Allocate (descLayoutMain); + dsGBuff = descriptorPool.Allocate (descLayoutGBuff); + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, samples); + cfg.Layout = new PipelineLayout (dev, descLayoutMain, descLayoutModelTextures, descLayoutGBuff); + cfg.Layout.AddPushConstants ( + new VkPushConstantRange (VkShaderStageFlags.Vertex, (uint)Marshal.SizeOf ()), + new VkPushConstantRange (VkShaderStageFlags.Fragment, (uint)Marshal.SizeOf (), 64) + ); + cfg.RenderPass = renderPass; + cfg.blendAttachments.Add (new VkPipelineColorBlendAttachmentState (false)); + cfg.blendAttachments.Add (new VkPipelineColorBlendAttachmentState (false)); + cfg.blendAttachments.Add (new VkPipelineColorBlendAttachmentState (false)); + + cfg.AddVertexBinding (0); + cfg.SetVertexAttributes (0, VkFormat.R32g32b32Sfloat, VkFormat.R32g32b32Sfloat, VkFormat.R32g32Sfloat); + cfg.AddShader (VkShaderStageFlags.Vertex, "shaders/pbrtest.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "shaders/GBuffPbr.frag.spv"); + + gBuffPipeline = new GraphicPipeline (cfg); + cfg.blendAttachments.Clear (); + cfg.blendAttachments.Add (new VkPipelineColorBlendAttachmentState (false)); + cfg.ResetShadersAndVerticesInfos (); + cfg.SubpassIndex = 1; + cfg.Layout = gBuffPipeline.Layout; + cfg.depthStencilState.depthTestEnable = false; + cfg.depthStencilState.depthWriteEnable = false; + cfg.AddShader (VkShaderStageFlags.Vertex, "shaders/FullScreenQuad.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "shaders/pbrtest.frag.spv"); + composePipeline = new GraphicPipeline (cfg); + + envCube = new EnvironmentCube (presentQueue, renderPass); + + uboMats = new HostBuffer (dev, VkBufferUsageFlags.UniformBuffer, (ulong)Marshal.SizeOf () * 2); + uboMats.Map ();//permanent map + + DescriptorSetWrites uboUpdate = new DescriptorSetWrites (descLayoutMain); + uboUpdate.Write (dev, dsMain, uboMats.Descriptor, + envCube.lutBrdf.Descriptor, + envCube.irradianceCube.Descriptor, + envCube.prefilterCube.Descriptor); + uboMats.Descriptor.offset = (ulong)Marshal.SizeOf (); + envCube.WriteDesc (uboMats.Descriptor); +#if DEBUG + debugDraw = new DebugDrawPipeline (dev, descLayoutMain, swapChain.ColorFormat); + debugDraw.AddLine (Vector3.Zero, new Vector3(matrices.lightPos.X,matrices.lightPos.Y,matrices.lightPos.Z)*3, 1, 1, 1); + debugDraw.AddLine (Vector3.Zero, Vector3.UnitX, 1, 0, 0); + debugDraw.AddLine (Vector3.Zero, Vector3.UnitY, 0, 1, 0); + debugDraw.AddLine (Vector3.Zero, Vector3.UnitZ, 0, 0, 1); +#endif + + + model = new Model (presentQueue, "../data/models/DamagedHelmet/glTF/DamagedHelmet.gltf"); + //model = new Model (presentQueue, "../data/models/chess.gltf"); + //model = new Model (presentQueue, "../data/models/Sponza/glTF/Sponza.gltf"); + //model = new Model (dev, presentQueue, "../data/models/icosphere.gltf"); + //model = new Model (dev, presentQueue, cmdPool, "../data/models/cube.gltf"); + model.WriteMaterialsDescriptorSets (descLayoutModelTextures, + VK.AttachmentType.Color, + VK.AttachmentType.Normal, + VK.AttachmentType.AmbientOcclusion, + VK.AttachmentType.PhysicalProps, + VK.AttachmentType.Emissive); + } + + void buildCommandBuffers () { + for (int i = 0; i < swapChain.ImageCount; ++i) { + cmds[i]?.Free (); + cmds[i] = cmdPool.AllocateCommandBuffer (); + cmds[i].Start (); + +#if DEBUG + statPool.Begin (cmds[i]); + recordDraw (cmds[i], frameBuffers[i]); + statPool.End (cmds[i]); + + debugDraw.RecordDraw (cmds[i], debugFB[i], camera); +#else + recordDraw (cmds[i], frameBuffers[i]); +#endif + + cmds[i].End (); + } + } + void recordDraw (CommandBuffer cmd, Framebuffer fb) { + renderPass.Begin (cmd, fb); + + cmd.SetViewport (fb.Width, fb.Height); + cmd.SetScissor (fb.Width, fb.Height); + + cmd.BindDescriptorSet (gBuffPipeline.Layout, dsMain); + gBuffPipeline.Bind (cmd); + model.Bind (cmd); + model.DrawAll (cmd, gBuffPipeline.Layout); + + renderPass.BeginSubPass (cmd); + + cmd.BindDescriptorSet (composePipeline.Layout, dsGBuff, 2); + composePipeline.Bind (cmd); + + cmd.Draw (3, 1, 0, 0); + + renderPass.End (cmd); + } + +#region update + void updateMatrices () { + camera.AspectRatio = (float)swapChain.Width / swapChain.Height; + + matrices.projection = camera.Projection; + matrices.view = camera.View; + matrices.model = camera.Model; + uboMats.Update (matrices, (uint)Marshal.SizeOf ()); + matrices.view *= Matrix4x4.CreateTranslation (-matrices.view.Translation); + matrices.model = Matrix4x4.Identity; + uboMats.Update (matrices, (uint)Marshal.SizeOf (), (uint)Marshal.SizeOf ()); + } + + public override void UpdateView () { + updateMatrices (); + updateViewRequested = false; + } + public override void Update () { +#if DEBUG + results = statPool.GetResults (); +#endif + } +#endregion + + + + void createGBuff () { + gbColorRough?.Dispose (); + gbEmitMetal?.Dispose (); + gbN?.Dispose (); + gbPos?.Dispose (); + + gbColorRough = new Image (dev, VkFormat.R8g8b8a8Unorm, VkImageUsageFlags.InputAttachment | VkImageUsageFlags.ColorAttachment, VkMemoryPropertyFlags.DeviceLocal, swapChain.Width, swapChain.Height); + gbEmitMetal = new Image (dev, VkFormat.R8g8b8a8Unorm, VkImageUsageFlags.InputAttachment | VkImageUsageFlags.ColorAttachment, VkMemoryPropertyFlags.DeviceLocal, swapChain.Width, swapChain.Height); + gbN = new Image (dev, VkFormat.R16g16b16a16Sfloat, VkImageUsageFlags.InputAttachment | VkImageUsageFlags.ColorAttachment, VkMemoryPropertyFlags.DeviceLocal, swapChain.Width, swapChain.Height); + gbPos = new Image (dev, VkFormat.R16g16b16a16Sfloat, VkImageUsageFlags.InputAttachment | VkImageUsageFlags.ColorAttachment, VkMemoryPropertyFlags.DeviceLocal, swapChain.Width, swapChain.Height); + + gbColorRough.CreateView (); + gbColorRough.CreateSampler (); + gbEmitMetal.CreateView (); + gbEmitMetal.CreateSampler (); + gbN.CreateView (); + gbN.CreateSampler (); + gbPos.CreateView (); + gbPos.CreateSampler (); + + DescriptorSetWrites uboUpdate = new DescriptorSetWrites (descLayoutGBuff); + uboUpdate.Write (dev, dsGBuff, gbColorRough.Descriptor, + gbEmitMetal.Descriptor, + gbN.Descriptor, + gbPos.Descriptor); + gbColorRough.SetName ("GBuffColorRough"); + gbEmitMetal.SetName ("GBuffEmitMetal"); + gbN.SetName ("GBuffN"); + gbPos.SetName ("GBuffPos"); + } + + protected override void OnResize () { + updateMatrices (); + + if (frameBuffers != null) + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); +#if DEBUG + if (debugFB != null) + for (int i = 0; i < swapChain.ImageCount; ++i) + debugFB[i]?.Dispose (); +#endif + + createGBuff (); + + frameBuffers = new Framebuffer[swapChain.ImageCount]; + debugFB = new Framebuffer[swapChain.ImageCount]; + + for (int i = 0; i < swapChain.ImageCount; ++i) { + frameBuffers[i] = new Framebuffer (renderPass, swapChain.Width, swapChain.Height, new Image[] { + swapChain.images[i], null, gbColorRough, gbEmitMetal, gbN, gbPos}); +#if DEBUG + debugFB[i] = new Framebuffer (debugDraw.RenderPass, swapChain.Width, swapChain.Height,(debugDraw.Samples == VkSampleCountFlags.SampleCount1) + ? new Image[] { swapChain.images[i] } : new Image[] { null, swapChain.images[i] }); + debugFB[i].SetName ("main FB " + i); +#endif + } + + buildCommandBuffers (); + } + + + #region Mouse and keyboard + protected override void onKeyDown (Key key, int scanCode, Modifier modifiers) { + switch (key) { + case Key.F1: + if (modifiers.HasFlag (Modifier.Shift)) + matrices.exposure -= 0.3f; + else + matrices.exposure += 0.3f; + break; + case Key.F2: + if (modifiers.HasFlag (Modifier.Shift)) + matrices.gamma -= 0.1f; + else + matrices.gamma += 0.1f; + break; + case Key.F3: + if (camera.Type == Camera.CamType.FirstPerson) + camera.Type = Camera.CamType.LookAt; + else + camera.Type = Camera.CamType.FirstPerson; + Console.WriteLine ($"camera type = {camera.Type}"); + break; + default: + base.onKeyDown (key, scanCode, modifiers); + return; + } + updateViewRequested = true; + } +#endregion + + protected override void Dispose (bool disposing) { + if (disposing) { + if (!isDisposed) { + dev.WaitIdle (); + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); + + gbColorRough.Dispose (); + gbEmitMetal.Dispose (); + gbN.Dispose (); + gbPos.Dispose (); + + gBuffPipeline.Dispose (); + composePipeline.Dispose (); + + descLayoutMain.Dispose (); + descLayoutModelTextures.Dispose (); + descLayoutGBuff.Dispose (); + + uboMats.Dispose (); + model.Dispose (); + envCube.Dispose (); + + descriptorPool.Dispose (); +#if DEBUG + debugDraw.Dispose (); + timestampQPool?.Dispose (); + statPool?.Dispose (); +#endif + } + } + + base.Dispose (disposing); + } + } +} diff --git a/samples/deferred/mainWithDebugDrawer.cs b/samples/deferred/mainWithDebugDrawer.cs new file mode 100644 index 0000000..f95ed0a --- /dev/null +++ b/samples/deferred/mainWithDebugDrawer.cs @@ -0,0 +1,412 @@ +using System; +using System.Numerics; +using System.Runtime.InteropServices; +using Glfw; +using VK; +using CVKL; + +namespace deferredDebug { + class Program : VkWindow{ + static void Main (string[] args) { + using (Program vke = new Program ()) { + vke.Run (); + } + } + + VkSampleCountFlags samples = VkSampleCountFlags.SampleCount1; + + public struct Matrices { + public Matrix4x4 projection; + public Matrix4x4 view; + public Matrix4x4 model; + public Vector4 lightPos; + public float gamma; + public float exposure; + } + + public Matrices matrices = new Matrices { + lightPos = new Vector4 (1.0f, 0, 0, 1.0f), + gamma = 1.0f, + exposure = 2.0f, + }; + +#if DEBUG + PipelineStatisticsQueryPool statPool; + TimestampQueryPool timestampQPool; + ulong[] results; +#endif + + protected override void configureEnabledFeatures (ref VkPhysicalDeviceFeatures features) { + base.configureEnabledFeatures (ref features); +#if DEBUG + features.pipelineStatisticsQuery = true; +#endif + } + + Program () : base(true) { + camera.Model = Matrix4x4.CreateRotationX (Utils.DegreesToRadians (-90)) * Matrix4x4.CreateRotationY (Utils.DegreesToRadians (180)); + camera.SetRotation (-0.1f,-0.4f); + camera.SetPosition (0, 0, -3); + + init (); + +#if DEBUG + statPool = new PipelineStatisticsQueryPool (dev, + VkQueryPipelineStatisticFlags.InputAssemblyVertices | + VkQueryPipelineStatisticFlags.InputAssemblyPrimitives | + VkQueryPipelineStatisticFlags.ClippingInvocations | + VkQueryPipelineStatisticFlags.ClippingPrimitives | + VkQueryPipelineStatisticFlags.FragmentShaderInvocations); + + timestampQPool = new TimestampQueryPool (dev); +#endif + } + + Framebuffer[] frameBuffers; + Image gbColorRough, gbEmitMetal, gbN, gbPos; + + DescriptorPool descriptorPool; + DescriptorSetLayout descLayoutMain, descLayoutModelTextures, descLayoutGBuff; + DescriptorSet dsMain, dsGBuff; + + Pipeline gBuffPipeline, composePipeline; + + HostBuffer uboMats; + + RenderPass renderPass; + + Model model; + EnvironmentCube envCube; + + DebugDrawPipeline debugDraw; + Framebuffer[] debugFB; + + void init () { + renderPass = new RenderPass (dev); + renderPass.AddAttachment (swapChain.ColorFormat, VkImageLayout.ColorAttachmentOptimal, VkSampleCountFlags.SampleCount1); + renderPass.AddAttachment (dev.GetSuitableDepthFormat(), VkImageLayout.DepthStencilAttachmentOptimal, samples); + renderPass.AddAttachment (VkFormat.R8g8b8a8Unorm, VkImageLayout.ColorAttachmentOptimal); + renderPass.AddAttachment (VkFormat.R8g8b8a8Unorm, VkImageLayout.ColorAttachmentOptimal); + renderPass.AddAttachment (VkFormat.R16g16b16a16Sfloat, VkImageLayout.ColorAttachmentOptimal); + renderPass.AddAttachment (VkFormat.R16g16b16a16Sfloat, VkImageLayout.ColorAttachmentOptimal); + + renderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0.0f, 0.0f, 0.0f) }); + renderPass.ClearValues.Add (new VkClearValue { depthStencil = new VkClearDepthStencilValue (1.0f, 0) }); + renderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0.0f, 0.0f, 0.0f) }); + renderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0.0f, 0.0f, 0.0f) }); + renderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0.0f, 0.0f, 0.0f) }); + renderPass.ClearValues.Add (new VkClearValue { color = new VkClearColorValue (0.0f, 0.0f, 0.0f) }); + + SubPass[] subpass = { new SubPass (), new SubPass () }; + subpass[0].AddColorReference ( new VkAttachmentReference (2, VkImageLayout.ColorAttachmentOptimal), + new VkAttachmentReference (3, VkImageLayout.ColorAttachmentOptimal), + new VkAttachmentReference (4, VkImageLayout.ColorAttachmentOptimal), + new VkAttachmentReference (5, VkImageLayout.ColorAttachmentOptimal)); + subpass[0].SetDepthReference (1, VkImageLayout.DepthStencilAttachmentOptimal); + + subpass[1].AddColorReference (0, VkImageLayout.ColorAttachmentOptimal); + subpass[1].AddInputReference ( new VkAttachmentReference (2, VkImageLayout.ShaderReadOnlyOptimal), + new VkAttachmentReference (3, VkImageLayout.ShaderReadOnlyOptimal), + new VkAttachmentReference (4, VkImageLayout.ShaderReadOnlyOptimal), + new VkAttachmentReference (5, VkImageLayout.ShaderReadOnlyOptimal)); + renderPass.AddSubpass (subpass); + + renderPass.AddDependency (Vk.SubpassExternal, 0, + VkPipelineStageFlags.BottomOfPipe, VkPipelineStageFlags.ColorAttachmentOutput, + VkAccessFlags.MemoryRead, VkAccessFlags.ColorAttachmentRead | VkAccessFlags.ColorAttachmentWrite); + renderPass.AddDependency (0, 1, + VkPipelineStageFlags.ColorAttachmentOutput, VkPipelineStageFlags.FragmentShader, + VkAccessFlags.ColorAttachmentWrite, VkAccessFlags.ShaderRead); + renderPass.AddDependency (1, Vk.SubpassExternal, + VkPipelineStageFlags.ColorAttachmentOutput, VkPipelineStageFlags.BottomOfPipe, + VkAccessFlags.ColorAttachmentRead | VkAccessFlags.ColorAttachmentWrite, VkAccessFlags.MemoryRead); + + + descriptorPool = new DescriptorPool (dev, 3, + new VkDescriptorPoolSize (VkDescriptorType.UniformBuffer, 2), + new VkDescriptorPoolSize (VkDescriptorType.CombinedImageSampler, 3), + new VkDescriptorPoolSize (VkDescriptorType.InputAttachment, 4) + ); + + descLayoutMain = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, VkDescriptorType.UniformBuffer), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (2, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (3, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler)); + + descLayoutModelTextures = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (2, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (3, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (4, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler) + ); + + descLayoutGBuff = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Fragment, VkDescriptorType.InputAttachment), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Fragment, VkDescriptorType.InputAttachment), + new VkDescriptorSetLayoutBinding (2, VkShaderStageFlags.Fragment, VkDescriptorType.InputAttachment), + new VkDescriptorSetLayoutBinding (3, VkShaderStageFlags.Fragment, VkDescriptorType.InputAttachment)); + + dsMain = descriptorPool.Allocate (descLayoutMain); + dsGBuff = descriptorPool.Allocate (descLayoutGBuff); + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, samples); + cfg.Layout = new PipelineLayout (dev, descLayoutMain, descLayoutModelTextures, descLayoutGBuff); + cfg.Layout.AddPushConstants ( + new VkPushConstantRange (VkShaderStageFlags.Vertex, (uint)Marshal.SizeOf ()), + new VkPushConstantRange (VkShaderStageFlags.Fragment, sizeof(int), 64) + ); + cfg.RenderPass = renderPass; + cfg.blendAttachments.Add (new VkPipelineColorBlendAttachmentState (false)); + cfg.blendAttachments.Add (new VkPipelineColorBlendAttachmentState (false)); + cfg.blendAttachments.Add (new VkPipelineColorBlendAttachmentState (false)); + + cfg.AddVertexBinding (0); + cfg.SetVertexAttributes (0, VkFormat.R32g32b32Sfloat, VkFormat.R32g32b32Sfloat, VkFormat.R32g32Sfloat); + cfg.AddShader (VkShaderStageFlags.Vertex, "shaders/pbrtest.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "shaders/GBuffPbr.frag.spv"); + + gBuffPipeline = new GraphicPipeline (cfg); + cfg.blendAttachments.Clear (); + cfg.blendAttachments.Add (new VkPipelineColorBlendAttachmentState (false)); + cfg.ResetShadersAndVerticesInfos (); + cfg.SubpassIndex = 1; + cfg.Layout = gBuffPipeline.Layout; + cfg.depthStencilState.depthTestEnable = false; + cfg.depthStencilState.depthWriteEnable = false; + cfg.AddShader (VkShaderStageFlags.Vertex, "shaders/FullScreenQuad.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "shaders/pbrtest.frag.spv"); + composePipeline = new GraphicPipeline (cfg); + + envCube = new EnvironmentCube (presentQueue, renderPass); + + uboMats = new HostBuffer (dev, VkBufferUsageFlags.UniformBuffer, (ulong)Marshal.SizeOf () * 2); + uboMats.Map ();//permanent map + + DescriptorSetWrites uboUpdate = new DescriptorSetWrites (descLayoutMain); + uboUpdate.Write (dev, dsMain, uboMats.Descriptor, + envCube.lutBrdf.Descriptor, + envCube.irradianceCube.Descriptor, + envCube.prefilterCube.Descriptor); + uboMats.Descriptor.offset = (ulong)Marshal.SizeOf (); + envCube.WriteDesc (uboMats.Descriptor); +#if DEBUG + debugDraw = new DebugDrawPipeline (dev, descLayoutMain, swapChain.ColorFormat); + debugDraw.AddLine (Vector3.Zero, new Vector3(matrices.lightPos.X,matrices.lightPos.Y,matrices.lightPos.Z)*3, 1, 1, 1); + debugDraw.AddLine (Vector3.Zero, Vector3.UnitX, 1, 0, 0); + debugDraw.AddLine (Vector3.Zero, Vector3.UnitY, 0, 1, 0); + debugDraw.AddLine (Vector3.Zero, Vector3.UnitZ, 0, 0, 1); +#endif + + + model = new Model (presentQueue, "../data/models/DamagedHelmet/glTF/DamagedHelmet.gltf"); + //model = new Model (presentQueue, "../data/models/chess.gltf"); + //model = new Model (presentQueue, "../data/models/Sponza/glTF/Sponza.gltf"); + //model = new Model (dev, presentQueue, "../data/models/icosphere.gltf"); + //model = new Model (dev, presentQueue, cmdPool, "../data/models/cube.gltf"); + model.WriteMaterialsDescriptorSets (descLayoutModelTextures, + VK.AttachmentType.Color, + VK.AttachmentType.Normal, + VK.AttachmentType.AmbientOcclusion, + VK.AttachmentType.PhysicalProps, + VK.AttachmentType.Emissive); + } + + void buildCommandBuffers () { + for (int i = 0; i < swapChain.ImageCount; ++i) { + cmds[i]?.Free (); + cmds[i] = cmdPool.AllocateCommandBuffer (); + cmds[i].Start (); + +#if DEBUG + statPool.Begin (cmds[i]); + recordDraw (cmds[i], frameBuffers[i]); + statPool.End (cmds[i]); + + debugDraw.RecordDraw (cmds[i], debugFB[i], camera); +#else + recordDraw (cmds[i], frameBuffers[i]); +#endif + + cmds[i].End (); + } + } + void recordDraw (CommandBuffer cmd, Framebuffer fb) { + renderPass.Begin (cmd, fb); + + cmd.SetViewport (fb.Width, fb.Height); + cmd.SetScissor (fb.Width, fb.Height); + + cmd.BindDescriptorSet (gBuffPipeline.Layout, dsMain); + gBuffPipeline.Bind (cmd); + model.Bind (cmd); + model.DrawAll (cmd, gBuffPipeline.Layout); + + renderPass.BeginSubPass (cmd); + + cmd.BindDescriptorSet (composePipeline.Layout, dsGBuff, 2); + composePipeline.Bind (cmd); + + cmd.Draw (3, 1, 0, 0); + + renderPass.End (cmd); + } + +#region update + void updateMatrices () { + camera.AspectRatio = (float)swapChain.Width / swapChain.Height; + + matrices.projection = camera.Projection; + matrices.view = camera.View; + matrices.model = camera.Model; + uboMats.Update (matrices, (uint)Marshal.SizeOf ()); + matrices.view *= Matrix4x4.CreateTranslation (-matrices.view.Translation); + matrices.model = Matrix4x4.Identity; + uboMats.Update (matrices, (uint)Marshal.SizeOf (), (uint)Marshal.SizeOf ()); + } + + public override void UpdateView () { + updateMatrices (); + updateViewRequested = false; + } + public override void Update () { +#if DEBUG + results = statPool.GetResults (); +#endif + } +#endregion + + + + void createGBuff () { + gbColorRough?.Dispose (); + gbEmitMetal?.Dispose (); + gbN?.Dispose (); + gbPos?.Dispose (); + + gbColorRough = new Image (dev, VkFormat.R8g8b8a8Unorm, VkImageUsageFlags.InputAttachment | VkImageUsageFlags.ColorAttachment, VkMemoryPropertyFlags.DeviceLocal, swapChain.Width, swapChain.Height); + gbEmitMetal = new Image (dev, VkFormat.R8g8b8a8Unorm, VkImageUsageFlags.InputAttachment | VkImageUsageFlags.ColorAttachment, VkMemoryPropertyFlags.DeviceLocal, swapChain.Width, swapChain.Height); + gbN = new Image (dev, VkFormat.R16g16b16a16Sfloat, VkImageUsageFlags.InputAttachment | VkImageUsageFlags.ColorAttachment, VkMemoryPropertyFlags.DeviceLocal, swapChain.Width, swapChain.Height); + gbPos = new Image (dev, VkFormat.R16g16b16a16Sfloat, VkImageUsageFlags.InputAttachment | VkImageUsageFlags.ColorAttachment, VkMemoryPropertyFlags.DeviceLocal, swapChain.Width, swapChain.Height); + + gbColorRough.CreateView (); + gbColorRough.CreateSampler (); + gbEmitMetal.CreateView (); + gbEmitMetal.CreateSampler (); + gbN.CreateView (); + gbN.CreateSampler (); + gbPos.CreateView (); + gbPos.CreateSampler (); + + DescriptorSetWrites uboUpdate = new DescriptorSetWrites (descLayoutGBuff); + uboUpdate.Write (dev, dsGBuff, gbColorRough.Descriptor, + gbEmitMetal.Descriptor, + gbN.Descriptor, + gbPos.Descriptor); + gbColorRough.SetName ("GBuffColorRough"); + gbEmitMetal.SetName ("GBuffEmitMetal"); + gbN.SetName ("GBuffN"); + gbPos.SetName ("GBuffPos"); + } + + protected override void OnResize () { + updateMatrices (); + + if (frameBuffers != null) + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); +#if DEBUG + if (debugFB != null) + for (int i = 0; i < swapChain.ImageCount; ++i) + debugFB[i]?.Dispose (); +#endif + + createGBuff (); + + frameBuffers = new Framebuffer[swapChain.ImageCount]; + debugFB = new Framebuffer[swapChain.ImageCount]; + + for (int i = 0; i < swapChain.ImageCount; ++i) { + frameBuffers[i] = new Framebuffer (renderPass, swapChain.Width, swapChain.Height, new Image[] { + swapChain.images[i], null, gbColorRough, gbEmitMetal, gbN, gbPos}); +#if DEBUG + debugFB[i] = new Framebuffer (debugDraw.RenderPass, swapChain.Width, swapChain.Height,(debugDraw.Samples == VkSampleCountFlags.SampleCount1) + ? new Image[] { swapChain.images[i] } : new Image[] { null, swapChain.images[i] }); + debugFB[i].SetName ("main FB " + i); +#endif + } + + buildCommandBuffers (); + } + + + #region Mouse and keyboard + protected override void onKeyDown (Key key, int scanCode, Modifier modifiers) { + switch (key) { + case Key.F1: + if (modifiers.HasFlag (Modifier.Shift)) + matrices.exposure -= 0.3f; + else + matrices.exposure += 0.3f; + break; + case Key.F2: + if (modifiers.HasFlag (Modifier.Shift)) + matrices.gamma -= 0.1f; + else + matrices.gamma += 0.1f; + break; + case Key.F3: + if (camera.Type == Camera.CamType.FirstPerson) + camera.Type = Camera.CamType.LookAt; + else + camera.Type = Camera.CamType.FirstPerson; + Console.WriteLine ($"camera type = {camera.Type}"); + break; + default: + base.onKeyDown (key, scanCode, modifiers); + return; + } + updateViewRequested = true; + } +#endregion + + protected override void Dispose (bool disposing) { + if (disposing) { + if (!isDisposed) { + dev.WaitIdle (); + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); + + gbColorRough.Dispose (); + gbEmitMetal.Dispose (); + gbN.Dispose (); + gbPos.Dispose (); + + gBuffPipeline.Dispose (); + composePipeline.Dispose (); + + descLayoutMain.Dispose (); + descLayoutModelTextures.Dispose (); + descLayoutGBuff.Dispose (); + + uboMats.Dispose (); + model.Dispose (); + envCube.Dispose (); + + descriptorPool.Dispose (); +#if DEBUG + foreach (Framebuffer fb in debugFB) + fb.Dispose (); + + debugDraw.Dispose (); + timestampQPool?.Dispose (); + statPool?.Dispose (); +#endif + } + } + + base.Dispose (disposing); + } + } +} diff --git a/samples/deferred/modelWithVkvgStats.cs b/samples/deferred/modelWithVkvgStats.cs new file mode 100644 index 0000000..f4ea123 --- /dev/null +++ b/samples/deferred/modelWithVkvgStats.cs @@ -0,0 +1,359 @@ +using System; +using System.Numerics; +using System.Runtime.InteropServices; +using Glfw; +using CVKL; +using VK; +using static CVKL.Camera; + +namespace modelWithVkvgStats { + class Program : VkWindow { + + PipelineStatisticsQueryPool statPool; + TimestampQueryPool timestampQPool; + + ulong[] results; + + protected override void configureEnabledFeatures (ref VkPhysicalDeviceFeatures features) { + base.configureEnabledFeatures (ref features); + features.pipelineStatisticsQuery = true; + } + public struct Matrices { + public Matrix4x4 projection; + public Matrix4x4 view; + public Matrix4x4 model; + public Vector4 lightPos; + public float gamma; + public float exposure; + } + + public Matrices matrices = new Matrices { + lightPos = new Vector4 (0.0f, 0.0f, -2.0f, 1.0f), + gamma = 1.0f, + exposure = 2.0f, + }; + + HostBuffer uboMats; + + DescriptorPool descriptorPool; + DescriptorSetLayout descLayoutMatrix; + DescriptorSetLayout descLayoutTextures; + DescriptorSet dsMats; + + GraphicPipeline pipeline; + GraphicPipeline uiPipeline; + Framebuffer[] frameBuffers; + + Model model; + + vkvg.Device vkvgDev; + vkvg.Surface vkvgSurf; + Image vkvgImage; + + void vkvgDraw () { + + using (vkvg.Context ctx = new vkvg.Context (vkvgSurf)) { + ctx.Operator = vkvg.Operator.Clear; + ctx.Paint (); + ctx.Operator = vkvg.Operator.Over; + + ctx.LineWidth = 1; + ctx.SetSource (0.1, 0.1, 0.1, 0.3); + ctx.Rectangle (5.5, 5.5, 400, 250); + ctx.FillPreserve (); + ctx.Flush (); + ctx.SetSource (0.8, 0.8, 0.8); + ctx.Stroke (); + + ctx.FontFace = "mono"; + ctx.FontSize = 10; + int x = 16; + int y = 40, dy = 16; + ctx.MoveTo (x, y); + ctx.ShowText (string.Format ($"fps: {fps,5} ")); + ctx.MoveTo (x + 200, y - 0.5); + ctx.Rectangle (x + 200, y - 8.5, 0.1 * fps, 10); + ctx.SetSource (0.1, 0.9, 0.1); + ctx.Fill (); + ctx.SetSource (0.8, 0.8, 0.8); + y += dy; + ctx.MoveTo (x, y); + ctx.ShowText (string.Format ($"Exposure:{matrices.exposure,5} ")); + y += dy; + ctx.MoveTo (x, y); + ctx.ShowText (string.Format ($"Gamma: {matrices.gamma,5} ")); + if (results == null) + return; + + y += dy*2; + ctx.MoveTo (x, y); + ctx.ShowText ("Pipeline Statistics"); + ctx.MoveTo (x-2, 2.5+y); + ctx.LineTo (x+160, 2.5+y); + ctx.Stroke (); + y += 4; + x += 20; + + for (int i = 0; i < statPool.RequestedStats.Length; i++) { + y += dy; + ctx.MoveTo (x, y); + ctx.ShowText (string.Format ($"{statPool.RequestedStats[i].ToString(),-30} :{results[i],12:0,0} ")); + } + + y += dy; + ctx.MoveTo (x, y); + ctx.ShowText (string.Format ($"{"Elapsed microsecond",-20} :{timestampQPool.ElapsedMiliseconds:0.0000} ")); + } + } + + Program () : base () { + vkvgDev = new vkvg.Device (instance.Handle, phy.Handle, dev.VkDev.Handle, presentQueue.qFamIndex, + vkvg.SampleCount.Sample_4, presentQueue.index); + + camera.Type = CamType.FirstPerson; + camera.Model = Matrix4x4.CreateScale (0.05f) * Matrix4x4.CreateRotationX((float)Math.PI); + //Camera.SetRotation (-0.1f,-0.4f); + camera.SetPosition (0, 2, -3); + + init (); + + model = new Model (presentQueue, "../data/models/Sponza/glTF/Sponza.gltf"); + model.WriteMaterialsDescriptorSets (descLayoutTextures, + VK.AttachmentType.Color, + VK.AttachmentType.Normal, + VK.AttachmentType.AmbientOcclusion, + VK.AttachmentType.PhysicalProps, + VK.AttachmentType.Emissive); + } + + void init (VkSampleCountFlags samples = VkSampleCountFlags.SampleCount4) { + descriptorPool = new DescriptorPool (dev, 2, + new VkDescriptorPoolSize (VkDescriptorType.UniformBuffer), + new VkDescriptorPoolSize (VkDescriptorType.CombinedImageSampler) + ); + + descLayoutMatrix = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Vertex|VkShaderStageFlags.Fragment, VkDescriptorType.UniformBuffer), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler) + ); + + descLayoutTextures = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (2, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (3, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler), + new VkDescriptorSetLayoutBinding (4, VkShaderStageFlags.Fragment, VkDescriptorType.CombinedImageSampler) + ); + + dsMats = descriptorPool.Allocate (descLayoutMatrix); + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList, samples); + + cfg.Layout = new PipelineLayout (dev, descLayoutMatrix, descLayoutTextures); + cfg.Layout.AddPushConstants ( + new VkPushConstantRange (VkShaderStageFlags.Vertex, (uint)Marshal.SizeOf ()), + new VkPushConstantRange (VkShaderStageFlags.Fragment, (uint)Marshal.SizeOf (), 64) + ); + cfg.RenderPass = new RenderPass (dev, swapChain.ColorFormat, dev.GetSuitableDepthFormat (), samples); + + cfg.AddVertexBinding (0); + cfg.SetVertexAttributes (0, VkFormat.R32g32b32Sfloat, VkFormat.R32g32b32Sfloat, VkFormat.R32g32Sfloat); + + cfg.AddShader (VkShaderStageFlags.Vertex, "shaders/pbrtest.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "shaders/pbrtest.frag.spv"); + + pipeline = new GraphicPipeline (cfg); + + cfg.ResetShadersAndVerticesInfos (); + cfg.AddShader (VkShaderStageFlags.Vertex, "shaders/FullScreenQuad.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Fragment, "shaders/simpletexture.frag.spv"); + + cfg.blendAttachments[0] = new VkPipelineColorBlendAttachmentState (true); + + uiPipeline = new GraphicPipeline (cfg); + + uboMats = new HostBuffer (dev, VkBufferUsageFlags.UniformBuffer, (ulong)Marshal.SizeOf()); + uboMats.Map ();//permanent map + + DescriptorSetWrites uboUpdate = new DescriptorSetWrites (dsMats, descLayoutMatrix.Bindings[0]); + uboUpdate.Write (dev, uboMats.Descriptor); + + cfg.Layout.SetName ("Main Pipeline layout"); + uboMats.SetName ("uboMats"); + descriptorPool.SetName ("main pool"); + descLayoutTextures.SetName ("descLayoutTextures"); + + statPool = new PipelineStatisticsQueryPool (dev, + VkQueryPipelineStatisticFlags.InputAssemblyVertices | + VkQueryPipelineStatisticFlags.InputAssemblyPrimitives | + VkQueryPipelineStatisticFlags.ClippingInvocations | + VkQueryPipelineStatisticFlags.ClippingPrimitives | + VkQueryPipelineStatisticFlags.FragmentShaderInvocations); + + timestampQPool = new TimestampQueryPool (dev); + + } + + void buildCommandBuffers () { + for (int i = 0; i < swapChain.ImageCount; ++i) { + cmds[i]?.Free (); + + cmds[i] = cmdPool.AllocateCommandBuffer (); + cmds[i].Start (); + + statPool.Begin (cmds[i]); + cmds[i].BeginRegion ("draw" + i, 0.5f, 1f, 0f); + cmds[i].Handle.SetDebugMarkerName (dev, "cmd Draw" + i); + recordDraw (cmds[i], frameBuffers[i]); + cmds[i].EndRegion (); + statPool.End (cmds[i]); + cmds[i].End (); + } + } + void recordDraw (CommandBuffer cmd, Framebuffer fb) { + cmd.BeginRegion ("models", 0.5f, 1f, 0f); + pipeline.RenderPass.Begin (cmd, fb); + + cmd.SetViewport (fb.Width, fb.Height); + cmd.SetScissor (fb.Width, fb.Height); + + cmd.BindDescriptorSet (pipeline.Layout, dsMats); + pipeline.Bind (cmd); + model.Bind (cmd); + model.DrawAll (cmd, pipeline.Layout); + + cmd.EndRegion (); + cmd.BeginRegion ("vkvg", 0.5f, 1f, 0f); + uiPipeline.Bind (cmd); + + timestampQPool.Start (cmd); + + vkvgImage.SetLayout (cmd, VkImageAspectFlags.Color, VkImageLayout.ColorAttachmentOptimal, VkImageLayout.ShaderReadOnlyOptimal, + VkPipelineStageFlags.ColorAttachmentOutput, VkPipelineStageFlags.FragmentShader); + + cmd.Draw (3, 1, 0, 0); + + vkvgImage.SetLayout (cmd, VkImageAspectFlags.Color, VkImageLayout.ShaderReadOnlyOptimal, VkImageLayout.ColorAttachmentOptimal, + VkPipelineStageFlags.FragmentShader, VkPipelineStageFlags.BottomOfPipe); + + timestampQPool.End (cmd); + + pipeline.RenderPass.End (cmd); + cmd.EndRegion (); + } + + #region update + void updateMatrices () { + + camera.AspectRatio = (float)swapChain.Width / swapChain.Height; + + matrices.projection = camera.Projection; + matrices.view = camera.View; + matrices.model = camera.Model; + uboMats.Update (matrices, (uint)Marshal.SizeOf ()); + } + + public override void UpdateView () { + updateMatrices (); + updateViewRequested = false; + } + public override void Update () { + results = statPool.GetResults (); + vkvgDraw (); + } + #endregion + + #region mouse and keyboard + protected override void onKeyDown (Key key, int scanCode, Modifier modifiers) { + switch (key) { + case Key.F1: + if (modifiers.HasFlag (Modifier.Shift)) + matrices.exposure -= 0.3f; + else + matrices.exposure += 0.3f; + break; + case Key.F2: + if (modifiers.HasFlag (Modifier.Shift)) + matrices.gamma -= 0.1f; + else + matrices.gamma += 0.1f; + break; + case Key.F3: + if (camera.Type == CamType.FirstPerson) + camera.Type = CamType.LookAt; + else + camera.Type = CamType.FirstPerson; + break; + default: + base.onKeyDown (key, scanCode, modifiers); + return; + } + updateViewRequested = true; + } + #endregion + + protected override void OnResize () { + + vkvgImage?.Dispose (); + vkvgSurf?.Dispose (); + vkvgSurf = new vkvg.Surface (vkvgDev, (int)swapChain.Width, (int)swapChain.Height); + vkvgImage = new Image (dev, new VkImage ((ulong)vkvgSurf.VkImage.ToInt64 ()), VkFormat.B8g8r8a8Unorm, + VkImageUsageFlags.ColorAttachment, (uint)vkvgSurf.Width, (uint)vkvgSurf.Height); + vkvgImage.CreateView (VkImageViewType.ImageView2D, VkImageAspectFlags.Color); + vkvgImage.CreateSampler (VkFilter.Nearest,VkFilter.Nearest, VkSamplerMipmapMode.Nearest, VkSamplerAddressMode.ClampToBorder); + + vkvgImage.Descriptor.imageLayout = VkImageLayout.ShaderReadOnlyOptimal; + DescriptorSetWrites uboUpdate = new DescriptorSetWrites (dsMats, descLayoutMatrix.Bindings[1]); + uboUpdate.Write (dev, vkvgImage.Descriptor); + + updateMatrices (); + + if (frameBuffers!=null) + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); + + frameBuffers = new Framebuffer[swapChain.ImageCount]; + + for (int i = 0; i < swapChain.ImageCount; ++i) { + frameBuffers[i] = new Framebuffer (pipeline.RenderPass, swapChain.Width, swapChain.Height, + (pipeline.Samples == VkSampleCountFlags.SampleCount1) ? new Image[] { + swapChain.images[i], + null + } : new Image[] { + null, + null, + swapChain.images[i] + }); + frameBuffers[i].SetName ("main FB " + i); + + } + + buildCommandBuffers (); + } + + protected override void Dispose (bool disposing) { + if (disposing) { + if (!isDisposed) { + dev.WaitIdle (); + for (int i = 0; i < swapChain.ImageCount; ++i) + frameBuffers[i]?.Dispose (); + model.Dispose (); + pipeline.Dispose (); + descLayoutMatrix.Dispose (); + descLayoutTextures.Dispose (); + descriptorPool.Dispose (); + + uboMats.Dispose (); + + vkvgSurf?.Dispose (); + vkvgDev.Dispose (); + + timestampQPool.Dispose (); + statPool.Dispose (); + } + } + + base.Dispose (disposing); + } + } +} diff --git a/samples/deferred/shaders/FullScreenQuad.vert b/samples/deferred/shaders/FullScreenQuad.vert new file mode 100644 index 0000000..826720b --- /dev/null +++ b/samples/deferred/shaders/FullScreenQuad.vert @@ -0,0 +1,10 @@ +#version 450 + +layout (location = 0) out vec2 outUV; + +void main() +{ + outUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); + gl_Position = vec4(outUV * 2.0f + -1.0f, 0.0f, 1.0f); + //gl_Position = vec4(outUV -1.0f, 0.0f, 1.0f); +} diff --git a/samples/deferred/shaders/GBuffPbr.frag b/samples/deferred/shaders/GBuffPbr.frag new file mode 100644 index 0000000..ca398e6 --- /dev/null +++ b/samples/deferred/shaders/GBuffPbr.frag @@ -0,0 +1,212 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +#define MANUAL_SRGB 0 +#define DEBUG 0 + +layout (constant_id = 0) const float NEAR_PLANE = 0.1f; +layout (constant_id = 1) const float FAR_PLANE = 256.0f; +layout (constant_id = 2) const int MAT_COUNT = 1; + +struct Material { + vec4 baseColorFactor; + vec4 emissiveFactor; + vec4 diffuseFactor; + vec4 specularFactor; + float workflow; + uint tex0; + uint tex1; + float metallicFactor; + float roughnessFactor; + float alphaMask; + float alphaMaskCutoff; + int pad0; +}; +const float M_PI = 3.141592653589793; +const float c_MinRoughness = 0.04; + +const float PBR_WORKFLOW_METALLIC_ROUGHNESS = 1.0; +const float PBR_WORKFLOW_SPECULAR_GLOSINESS = 2.0f; + +const uint MAP_COLOR = 0x1; +const uint MAP_NORMAL = 0x2; +const uint MAP_AO = 0x4; +const uint MAP_METAL = 0x8; +const uint MAP_ROUGHNESS = 0x10; +const uint MAP_METALROUGHNESS = 0x20; +const uint MAP_EMISSIVE = 0x40; + +layout (location = 0) in vec3 inWorldPos; +layout (location = 1) in vec3 inNormal; +layout (location = 2) in vec2 inUV0; +layout (location = 3) in vec2 inUV1; + + +layout (set = 0, binding = 5) uniform UBOMaterials { + Material materials[MAT_COUNT]; +}; + +// Material bindings +layout (set = 2, binding = 0) uniform sampler2D colorMap; +layout (set = 2, binding = 1) uniform sampler2D physicalDescriptorMap; +layout (set = 2, binding = 2) uniform sampler2D normalMap; +layout (set = 2, binding = 3) uniform sampler2D aoMap; +layout (set = 2, binding = 4) uniform sampler2D emissiveMap; + + +layout (push_constant) uniform PushCsts { + layout(offset = 64) + int materialIdx; +}; + + +layout (location = 0) out vec4 outColorRough; +layout (location = 1) out vec4 outEmitMetal; +layout (location = 2) out vec4 outN_AO; +layout (location = 3) out vec4 outPos; + +vec4 SRGBtoLINEAR(vec4 srgbIn) +{ + #ifdef MANUAL_SRGB + #ifdef SRGB_FAST_APPROXIMATION + vec3 linOut = pow(srgbIn.xyz,vec3(2.2)); + #else //SRGB_FAST_APPROXIMATION + vec3 bLess = step(vec3(0.04045),srgbIn.xyz); + vec3 linOut = mix( srgbIn.xyz/vec3(12.92), pow((srgbIn.xyz+vec3(0.055))/vec3(1.055),vec3(2.4)), bLess ); + #endif //SRGB_FAST_APPROXIMATION + return vec4(linOut,srgbIn.w);; + #else //MANUAL_SRGB + return srgbIn; + #endif //MANUAL_SRGB +} + +// Find the normal for this fragment, pulling either from a predefined normal map +// or from the interpolated mesh normal and tangent attributes. +vec3 getNormal() +{ + vec3 tangentNormal; + // Perturb normal, see http://www.thetenthplanet.de/archives/1180 + if ((materials[materialIdx].tex0 & MAP_NORMAL) == MAP_NORMAL) + tangentNormal = texture(normalMap, inUV0).xyz * 2.0 - 1.0; + else if ((materials[materialIdx].tex1 & MAP_NORMAL) == MAP_NORMAL) + tangentNormal = texture(normalMap, inUV1).xyz * 2.0 - 1.0; + else + return normalize(inNormal); + + vec3 q1 = dFdx(inWorldPos); + vec3 q2 = dFdy(inWorldPos); + vec2 st1 = dFdx(inUV0); + vec2 st2 = dFdy(inUV0); + + vec3 N = normalize(inNormal); + vec3 T = normalize(q1 * st2.t - q2 * st1.t); + vec3 B = -normalize(cross(N, T)); + mat3 TBN = mat3(T, B, N); + + return normalize(TBN * tangentNormal); +} + +// Gets metallic factor from specular glossiness workflow inputs +float convertMetallic(vec3 diffuse, vec3 specular, float maxSpecular) { + float perceivedDiffuse = sqrt(0.299 * diffuse.r * diffuse.r + 0.587 * diffuse.g * diffuse.g + 0.114 * diffuse.b * diffuse.b); + float perceivedSpecular = sqrt(0.299 * specular.r * specular.r + 0.587 * specular.g * specular.g + 0.114 * specular.b * specular.b); + if (perceivedSpecular < c_MinRoughness) { + return 0.0; + } + float a = c_MinRoughness; + float b = perceivedDiffuse * (1.0 - maxSpecular) / (1.0 - c_MinRoughness) + perceivedSpecular - 2.0 * c_MinRoughness; + float c = c_MinRoughness - perceivedSpecular; + float D = max(b * b - 4.0 * a * c, 0.0); + return clamp((-b + sqrt(D)) / (2.0 * a), 0.0, 1.0); +} + +float linearDepth(float depth) +{ + float z = depth * 2.0f - 1.0f; + return (2.0f * NEAR_PLANE * FAR_PLANE) / (FAR_PLANE + NEAR_PLANE - z * (FAR_PLANE - NEAR_PLANE)); +} + +void main() +{ + float perceptualRoughness; + float metallic; + vec4 baseColor; + vec3 emissive = vec3(0); + + baseColor = materials[materialIdx].baseColorFactor; + + if (materials[materialIdx].workflow == PBR_WORKFLOW_METALLIC_ROUGHNESS) { + perceptualRoughness = materials[materialIdx].roughnessFactor; + metallic = materials[materialIdx].metallicFactor; + // Roughness is stored in the 'g' channel, metallic is stored in the 'b' channel. + // This layout intentionally reserves the 'r' channel for (optional) occlusion map data + if ((materials[materialIdx].tex0 & MAP_METALROUGHNESS) == MAP_METALROUGHNESS){ + perceptualRoughness *= texture(physicalDescriptorMap, inUV0).g; + metallic *= texture(physicalDescriptorMap, inUV0).b; + }else if ((materials[materialIdx].tex1 & MAP_METALROUGHNESS) == MAP_METALROUGHNESS){ + perceptualRoughness *= texture(physicalDescriptorMap, inUV1).g; + metallic *= texture(physicalDescriptorMap, inUV1).b; + } + perceptualRoughness = clamp(perceptualRoughness, c_MinRoughness, 1.0); + metallic = clamp(metallic, 0.0, 1.0); + + // The albedo may be defined from a base texture or a flat color + if ((materials[materialIdx].tex0 & MAP_COLOR) == MAP_COLOR) + baseColor *= SRGBtoLINEAR(texture(colorMap, inUV0)); + else if ((materials[materialIdx].tex1 & MAP_COLOR) == MAP_COLOR) + baseColor *= SRGBtoLINEAR(texture(colorMap, inUV1)); + } + + if (materials[materialIdx].alphaMask == 1.0f) { + if (baseColor.a < materials[materialIdx].alphaMaskCutoff) + discard; + } + + if (materials[materialIdx].workflow == PBR_WORKFLOW_SPECULAR_GLOSINESS) { + // Values from specular glossiness workflow are converted to metallic roughness + if ((materials[materialIdx].tex0 & MAP_METALROUGHNESS) == MAP_METALROUGHNESS) + perceptualRoughness = 1.0 - texture(physicalDescriptorMap, inUV0).a; + else if ((materials[materialIdx].tex1 & MAP_METALROUGHNESS) == MAP_METALROUGHNESS) + perceptualRoughness = 1.0 - texture(physicalDescriptorMap, inUV1).a; + else + perceptualRoughness = 0.0; + + const float epsilon = 1e-6; + + vec4 diffuse = SRGBtoLINEAR(texture(colorMap, inUV0)); + vec3 specular = SRGBtoLINEAR(texture(physicalDescriptorMap, inUV0)).rgb; + + float maxSpecular = max(max(specular.r, specular.g), specular.b); + + // Convert metallic value from specular glossiness inputs + metallic = convertMetallic(diffuse.rgb, specular, maxSpecular); + + vec3 baseColorDiffusePart = diffuse.rgb * ((1.0 - maxSpecular) / (1 - c_MinRoughness) / max(1 - metallic, epsilon)) * materials[materialIdx].diffuseFactor.rgb; + vec3 baseColorSpecularPart = specular - (vec3(c_MinRoughness) * (1 - metallic) * (1 / max(metallic, epsilon))) * materials[materialIdx].specularFactor.rgb; + baseColor = vec4(mix(baseColorDiffusePart, baseColorSpecularPart, metallic * metallic), diffuse.a); + + } + + const float u_OcclusionStrength = 1.0f; + const float u_EmissiveFactor = 1.0f; + float ao = 1.0f; + + if ((materials[materialIdx].tex0 & MAP_EMISSIVE) == MAP_EMISSIVE) + emissive = SRGBtoLINEAR(texture(emissiveMap, inUV0)).rgb * u_EmissiveFactor; + else if ((materials[materialIdx].tex1 & MAP_EMISSIVE) == MAP_EMISSIVE) + emissive = SRGBtoLINEAR(texture(emissiveMap, inUV1)).rgb * u_EmissiveFactor; + + if ((materials[materialIdx].tex0 & MAP_AO) == MAP_AO) + ao = texture(aoMap, inUV0).r; + else if ((materials[materialIdx].tex1 & MAP_AO) == MAP_AO) + ao = texture(aoMap, inUV1).r; + + vec3 n = getNormal(); + vec3 p = inWorldPos; + + outColorRough = vec4 (baseColor.rgb, perceptualRoughness); + outEmitMetal = vec4 (emissive, metallic); + outN_AO = vec4 (n, ao); + outPos = vec4 (p, linearDepth(gl_FragCoord.z)); +} \ No newline at end of file diff --git a/samples/deferred/shaders/GBuffPbr.vert b/samples/deferred/shaders/GBuffPbr.vert new file mode 100644 index 0000000..7cafe49 --- /dev/null +++ b/samples/deferred/shaders/GBuffPbr.vert @@ -0,0 +1,37 @@ +#version 450 + +layout (location = 0) in vec3 inPos; +layout (location = 1) in vec3 inNormal; +layout (location = 2) in vec2 inUV0; +layout (location = 3) in vec2 inUV1; + +layout (set = 0, binding = 0) uniform UBO { + mat4 projection; + mat4 model; + mat4 view; +} ubo; + +layout(push_constant) uniform PushConsts { + mat4 model; +} pc; + +layout (location = 0) out vec3 outWorldPos; +layout (location = 1) out vec3 outNormal; +layout (location = 2) out vec2 outUV0; +layout (location = 3) out vec2 outUV1; + +out gl_PerVertex +{ + vec4 gl_Position; +}; + +void main() +{ + vec4 locPos = ubo.model * pc.model * vec4(inPos, 1.0); + outNormal = normalize(transpose(inverse(mat3(ubo.model * pc.model))) * inNormal); + + outWorldPos = locPos.xyz; + outUV0 = inUV0; + outUV1 = inUV1; + gl_Position = ubo.projection * ubo.view * vec4(outWorldPos, 1.0); +} diff --git a/samples/deferred/shaders/GBuffPbrTexArray.frag b/samples/deferred/shaders/GBuffPbrTexArray.frag new file mode 100644 index 0000000..dee3471 --- /dev/null +++ b/samples/deferred/shaders/GBuffPbrTexArray.frag @@ -0,0 +1,211 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +#define MANUAL_SRGB 0 +#define DEBUG 0 + +layout (constant_id = 0) const float NEAR_PLANE = 0.1f; +layout (constant_id = 1) const float FAR_PLANE = 256.0f; +layout (constant_id = 2) const int MAT_COUNT = 1; + +struct Material { + vec4 baseColorFactor; + vec4 emissiveFactor; + vec4 diffuseFactor; + vec4 specularFactor; + + float workflow; + uint tex0; + uint tex1; + int baseColorTex; + + int physicalDescTex; + int normalTex; + int occlusionTex; + int emissiveTex; + + float metallicFactor; + float roughnessFactor; + float alphaMask; + float alphaMaskCutoff; +}; +const float M_PI = 3.141592653589793; +const float c_MinRoughness = 0.04; + +const float PBR_WORKFLOW_METALLIC_ROUGHNESS = 1.0; +const float PBR_WORKFLOW_SPECULAR_GLOSINESS = 2.0f; + +const uint MAP_COLOR = 0x1; +const uint MAP_NORMAL = 0x2; +const uint MAP_AO = 0x4; +const uint MAP_METAL = 0x8; +const uint MAP_ROUGHNESS = 0x10; +const uint MAP_METALROUGHNESS = 0x20; +const uint MAP_EMISSIVE = 0x40; + +layout (location = 0) in vec3 inWorldPos; +layout (location = 1) in vec3 inNormal; +layout (location = 2) in vec2 inUV0; +layout (location = 3) in vec2 inUV1; + +layout (set = 0, binding = 5) uniform UBOMaterials { + Material materials[MAT_COUNT]; +}; +layout (set = 0, binding = 7) uniform sampler2DArray texArray; + +layout (push_constant) uniform PushCsts { + layout(offset = 64) + int materialIdx; +}; + + +layout (location = 0) out vec4 outColorRough; +layout (location = 1) out vec4 outEmitMetal; +layout (location = 2) out vec4 outN_AO; +layout (location = 3) out vec4 outPos; + +vec4 SRGBtoLINEAR(vec4 srgbIn) +{ + #ifdef MANUAL_SRGB + #ifdef SRGB_FAST_APPROXIMATION + vec3 linOut = pow(srgbIn.xyz,vec3(2.2)); + #else //SRGB_FAST_APPROXIMATION + vec3 bLess = step(vec3(0.04045),srgbIn.xyz); + vec3 linOut = mix( srgbIn.xyz/vec3(12.92), pow((srgbIn.xyz+vec3(0.055))/vec3(1.055),vec3(2.4)), bLess ); + #endif //SRGB_FAST_APPROXIMATION + return vec4(linOut,srgbIn.w);; + #else //MANUAL_SRGB + return srgbIn; + #endif //MANUAL_SRGB +} + +// Find the normal for this fragment, pulling either from a predefined normal map +// or from the interpolated mesh normal and tangent attributes. +vec3 getNormal() +{ + vec3 tangentNormal; + // Perturb normal, see http://www.thetenthplanet.de/archives/1180 + if ((materials[materialIdx].tex0 & MAP_NORMAL) == MAP_NORMAL) + tangentNormal = texture(texArray, vec3(inUV0, materials[materialIdx].normalTex)).xyz * 2.0 - 1.0; + else if ((materials[materialIdx].tex1 & MAP_NORMAL) == MAP_NORMAL) + tangentNormal = texture(texArray, vec3(inUV1, materials[materialIdx].normalTex)).xyz * 2.0 - 1.0; + else + return normalize(inNormal); + + vec3 q1 = dFdx(inWorldPos); + vec3 q2 = dFdy(inWorldPos); + vec2 st1 = dFdx(inUV0); + vec2 st2 = dFdy(inUV0); + + vec3 N = normalize(inNormal); + vec3 T = normalize(q1 * st2.t - q2 * st1.t); + vec3 B = -normalize(cross(N, T)); + mat3 TBN = mat3(T, B, N); + + return normalize(TBN * tangentNormal); +} + +// Gets metallic factor from specular glossiness workflow inputs +float convertMetallic(vec3 diffuse, vec3 specular, float maxSpecular) { + float perceivedDiffuse = sqrt(0.299 * diffuse.r * diffuse.r + 0.587 * diffuse.g * diffuse.g + 0.114 * diffuse.b * diffuse.b); + float perceivedSpecular = sqrt(0.299 * specular.r * specular.r + 0.587 * specular.g * specular.g + 0.114 * specular.b * specular.b); + if (perceivedSpecular < c_MinRoughness) { + return 0.0; + } + float a = c_MinRoughness; + float b = perceivedDiffuse * (1.0 - maxSpecular) / (1.0 - c_MinRoughness) + perceivedSpecular - 2.0 * c_MinRoughness; + float c = c_MinRoughness - perceivedSpecular; + float D = max(b * b - 4.0 * a * c, 0.0); + return clamp((-b + sqrt(D)) / (2.0 * a), 0.0, 1.0); +} + +float linearDepth(float depth) +{ + float z = depth * 2.0f - 1.0f; + return (2.0f * NEAR_PLANE * FAR_PLANE) / (FAR_PLANE + NEAR_PLANE - z * (FAR_PLANE - NEAR_PLANE)); +} + +void main() +{ + float perceptualRoughness; + float metallic; + vec4 baseColor; + vec3 emissive = vec3(0); + + baseColor = materials[materialIdx].baseColorFactor; + + if (materials[materialIdx].workflow == PBR_WORKFLOW_METALLIC_ROUGHNESS) { + perceptualRoughness = materials[materialIdx].roughnessFactor; + metallic = materials[materialIdx].metallicFactor; + // Roughness is stored in the 'g' channel, metallic is stored in the 'b' channel. + // This layout intentionally reserves the 'r' channel for (optional) occlusion map data + if ((materials[materialIdx].tex0 & MAP_METALROUGHNESS) == MAP_METALROUGHNESS){ + perceptualRoughness *= texture(texArray, vec3(inUV0, materials[materialIdx].physicalDescTex)).g; + metallic *= texture(texArray, vec3(inUV0, materials[materialIdx].physicalDescTex)).b; + }else if ((materials[materialIdx].tex1 & MAP_METALROUGHNESS) == MAP_METALROUGHNESS){ + perceptualRoughness *= texture(texArray, vec3(inUV1, materials[materialIdx].physicalDescTex)).g; + metallic *= texture(texArray, vec3(inUV1, materials[materialIdx].physicalDescTex)).b; + } + perceptualRoughness = clamp(perceptualRoughness, c_MinRoughness, 1.0); + metallic = clamp(metallic, 0.0, 1.0); + + // The albedo may be defined from a base texture or a flat color + if ((materials[materialIdx].tex0 & MAP_COLOR) == MAP_COLOR) + baseColor *= SRGBtoLINEAR(texture(texArray, vec3(inUV0, materials[materialIdx].baseColorTex))); + else if ((materials[materialIdx].tex1 & MAP_COLOR) == MAP_COLOR) + baseColor *= SRGBtoLINEAR(texture(texArray, vec3(inUV1, materials[materialIdx].baseColorTex))); + } + + if (materials[materialIdx].alphaMask == 1.0f) { + if (baseColor.a < materials[materialIdx].alphaMaskCutoff) + discard; + } + + if (materials[materialIdx].workflow == PBR_WORKFLOW_SPECULAR_GLOSINESS) { + // Values from specular glossiness workflow are converted to metallic roughness + if ((materials[materialIdx].tex0 & MAP_METALROUGHNESS) == MAP_METALROUGHNESS) + perceptualRoughness = 1.0 - texture(texArray, vec3(inUV0, materials[materialIdx].physicalDescTex)).a; + else if ((materials[materialIdx].tex1 & MAP_METALROUGHNESS) == MAP_METALROUGHNESS) + perceptualRoughness = 1.0 - texture(texArray, vec3(inUV1, materials[materialIdx].physicalDescTex)).a; + else + perceptualRoughness = 0.0; + + const float epsilon = 1e-6; + + vec4 diffuse = SRGBtoLINEAR(texture(texArray, vec3(inUV0, materials[materialIdx].baseColorTex))); + vec3 specular = SRGBtoLINEAR(texture(texArray, vec3(inUV0, materials[materialIdx].physicalDescTex))).rgb; + + float maxSpecular = max(max(specular.r, specular.g), specular.b); + + // Convert metallic value from specular glossiness inputs + metallic = convertMetallic(diffuse.rgb, specular, maxSpecular); + + vec3 baseColorDiffusePart = diffuse.rgb * ((1.0 - maxSpecular) / (1 - c_MinRoughness) / max(1 - metallic, epsilon)) * materials[materialIdx].diffuseFactor.rgb; + vec3 baseColorSpecularPart = specular - (vec3(c_MinRoughness) * (1 - metallic) * (1 / max(metallic, epsilon))) * materials[materialIdx].specularFactor.rgb; + baseColor = vec4(mix(baseColorDiffusePart, baseColorSpecularPart, metallic * metallic), diffuse.a); + + } + + const float u_OcclusionStrength = 1.0f; + const float u_EmissiveFactor = 1.0f; + float ao = 1.0f; + + if ((materials[materialIdx].tex0 & MAP_EMISSIVE) == MAP_EMISSIVE) + emissive = SRGBtoLINEAR(texture(texArray, vec3(inUV0, materials[materialIdx].emissiveTex))).rgb * u_EmissiveFactor; + else if ((materials[materialIdx].tex1 & MAP_EMISSIVE) == MAP_EMISSIVE) + emissive = SRGBtoLINEAR(texture(texArray, vec3(inUV1, materials[materialIdx].emissiveTex))).rgb * u_EmissiveFactor; + + if ((materials[materialIdx].tex0 & MAP_AO) == MAP_AO) + ao = texture(texArray, vec3(inUV0, materials[materialIdx].occlusionTex)).r; + else if ((materials[materialIdx].tex1 & MAP_AO) == MAP_AO) + ao = texture(texArray, vec3(inUV1, materials[materialIdx].occlusionTex)).r; + + vec3 n = getNormal(); + vec3 p = inWorldPos; + + outColorRough = vec4 (baseColor.rgb, perceptualRoughness); + outEmitMetal = vec4 (emissive, metallic); + outN_AO = vec4 (n, ao); + outPos = vec4 (p, linearDepth(gl_FragCoord.z)); +} \ No newline at end of file diff --git a/samples/deferred/shaders/bloom.comp b/samples/deferred/shaders/bloom.comp new file mode 100644 index 0000000..e2b1318 --- /dev/null +++ b/samples/deferred/shaders/bloom.comp @@ -0,0 +1,60 @@ +#version 450 + +layout (local_size_x = 16, local_size_y = 16) in; +layout (binding = 0, rgba16f) uniform readonly image2D inputImage; +layout (binding = 1, rgba16f) uniform image2D resultImage; + +layout(push_constant) uniform PushConsts { + vec2 texSize; + int dir; + float scale; + float strength; +} pc; + +void main(void) +{ + // From the OpenGL Super bible + const float weights[] = float[](0.0024499299678342, + 0.0043538453346397, + 0.0073599963704157, + 0.0118349786570722, + 0.0181026699707781, + 0.0263392293891488, + 0.0364543006660986, + 0.0479932050577658, + 0.0601029809166942, + 0.0715974486241365, + 0.0811305381519717, + 0.0874493212267511, + 0.0896631113333857, + 0.0874493212267511, + 0.0811305381519717, + 0.0715974486241365, + 0.0601029809166942, + 0.0479932050577658, + 0.0364543006660986, + 0.0263392293891488, + 0.0181026699707781, + 0.0118349786570722, + 0.0073599963704157, + 0.0043538453346397, + 0.0024499299678342); + + + float ar = 1.0; + vec4 color = vec4(0.0); + + // Aspect ratio for vertical blur pass + if (pc.dir == 1) + ar = pc.texSize.y / pc.texSize.x; + + vec2 P = gl_GlobalInvocationID.yx - vec2(0, (weights.length() >> 1) * ar * pc.scale); + + for (int i = 0; i < weights.length(); i++) + { + vec2 dv = vec2(0.0, i * pc.scale) * ar; + color += imageLoad (inputImage, ivec2(P + dv)) * weights[i] * pc.strength; + } + + imageStore(resultImage, ivec2(gl_GlobalInvocationID.xy), color); +} \ No newline at end of file diff --git a/samples/deferred/shaders/bloom.frag b/samples/deferred/shaders/bloom.frag new file mode 100644 index 0000000..c3bf402 --- /dev/null +++ b/samples/deferred/shaders/bloom.frag @@ -0,0 +1,63 @@ +#version 450 + +layout (binding = 0) uniform sampler2D samplerColor0; +layout (binding = 1) uniform sampler2D samplerColor1; + +layout (location = 0) in vec2 inUV; + +layout (location = 0) out vec4 outColor; + +layout (constant_id = 0) const int dir = 0; + +void main(void) +{ + // From the OpenGL Super bible + const float weights[] = float[](0.0024499299678342, + 0.0043538453346397, + 0.0073599963704157, + 0.0118349786570722, + 0.0181026699707781, + 0.0263392293891488, + 0.0364543006660986, + 0.0479932050577658, + 0.0601029809166942, + 0.0715974486241365, + 0.0811305381519717, + 0.0874493212267511, + 0.0896631113333857, + 0.0874493212267511, + 0.0811305381519717, + 0.0715974486241365, + 0.0601029809166942, + 0.0479932050577658, + 0.0364543006660986, + 0.0263392293891488, + 0.0181026699707781, + 0.0118349786570722, + 0.0073599963704157, + 0.0043538453346397, + 0.0024499299678342); + + + const float blurScale = 0.003; + const float blurStrength = 1.0; + + float ar = 1.0; + // Aspect ratio for vertical blur pass + if (dir == 1) + { + vec2 ts = textureSize(samplerColor1, 0); + ar = ts.y / ts.x; + } + + vec2 P = inUV.yx - vec2(0, (weights.length() >> 1) * ar * blurScale); + + vec4 color = vec4(0.0); + for (int i = 0; i < weights.length(); i++) + { + vec2 dv = vec2(0.0, i * blurScale) * ar; + color += texture(samplerColor1, P + dv) * weights[i] * blurStrength; + } + + outColor = color; +} \ No newline at end of file diff --git a/samples/deferred/shaders/compose.frag b/samples/deferred/shaders/compose.frag new file mode 100644 index 0000000..81312bd --- /dev/null +++ b/samples/deferred/shaders/compose.frag @@ -0,0 +1,230 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (set = 0, binding = 0) uniform UBO { + mat4 projection; + mat4 model; + mat4 view; + vec4 camPos; + float exposure; + float gamma; + float prefilteredCubeMipLevels; + float scaleIBLAmbient; +} ubo; + +layout (constant_id = 0) const uint NUM_LIGHTS = 1; + +struct Light { + vec4 position; + vec4 color; + mat4 mvp; +}; + +layout (set = 0, binding = 4) uniform UBOLights { + Light lights[NUM_LIGHTS]; +}; + +const float M_PI = 3.141592653589793; +const float c_MinRoughness = 0.04; + +layout (input_attachment_index = 0, set = 1, binding = 0) uniform subpassInputMS samplerColorRough; +layout (input_attachment_index = 1, set = 1, binding = 1) uniform subpassInputMS samplerEmitMetal; +layout (input_attachment_index = 2, set = 1, binding = 2) uniform subpassInputMS samplerN_AO; +layout (input_attachment_index = 3, set = 1, binding = 3) uniform subpassInputMS samplerPos; + +layout (set = 0, binding = 1) uniform samplerCube samplerIrradiance; +layout (set = 0, binding = 2) uniform samplerCube prefilteredMap; +layout (set = 0, binding = 3) uniform sampler2D samplerBRDFLUT; + +layout (location = 0) in vec2 inUV; + +layout (location = 0) out vec4 outColor; + +// Encapsulate the various inputs used by the various functions in the shading equation +// We store values in this struct to simplify the integration of alternative implementations +// of the shading terms, outlined in the Readme.MD Appendix. +struct PBRInfo +{ + float NdotL; // cos angle between normal and light direction + float NdotV; // cos angle between normal and view direction + float NdotH; // cos angle between normal and half vector + float LdotH; // cos angle between light direction and half vector + float VdotH; // cos angle between view direction and half vector + float perceptualRoughness; // roughness value, as authored by the model creator (input to shader) + float metalness; // metallic value at the surface + vec3 reflectance0; // full reflectance color (normal incidence angle) + vec3 reflectance90; // reflectance color at grazing angle + float alphaRoughness; // roughness mapped to a more linear change in the roughness (proposed by [2]) + vec3 diffuseColor; // color contribution from diffuse lighting + vec3 specularColor; // color contribution from specular lighting +}; + +// Calculation of the lighting contribution from an optional Image Based Light source. +// Precomputed Environment Maps are required uniform inputs and are computed as outlined in [1]. +// See our README.md on Environment Maps [3] for additional discussion. +vec3 getIBLContribution(PBRInfo pbrInputs, vec3 n, vec3 reflection) +{ + float lod = (pbrInputs.perceptualRoughness * ubo.prefilteredCubeMipLevels); + // retrieve a scale and bias to F0. See [1], Figure 3 + vec3 brdf = (texture(samplerBRDFLUT, vec2(pbrInputs.NdotV, 1.0 - pbrInputs.perceptualRoughness))).rgb; + vec3 diffuseLight = texture(samplerIrradiance, reflection).rgb; + + vec3 specularLight = textureLod(prefilteredMap, reflection, lod).rgb; + + vec3 diffuse = diffuseLight * pbrInputs.diffuseColor; + vec3 specular = specularLight * (pbrInputs.specularColor * brdf.x + brdf.y); + + // For presentation, this allows us to disable IBL terms + // For presentation, this allows us to disable IBL terms + diffuse *= ubo.scaleIBLAmbient; + specular *= ubo.scaleIBLAmbient; + + return diffuse + specular; +} + +// Basic Lambertian diffuse +// Implementation from Lambert's Photometria https://archive.org/details/lambertsphotome00lambgoog +// See also [1], Equation 1 +vec3 diffuse(PBRInfo pbrInputs) +{ + return pbrInputs.diffuseColor / M_PI; +} + +// The following equation models the Fresnel reflectance term of the spec equation (aka F()) +// Implementation of fresnel from [4], Equation 15 +vec3 specularReflection(PBRInfo pbrInputs) +{ + return pbrInputs.reflectance0 + (pbrInputs.reflectance90 - pbrInputs.reflectance0) * pow(clamp(1.0 - pbrInputs.VdotH, 0.0, 1.0), 5.0); +} + +// This calculates the specular geometric attenuation (aka G()), +// where rougher material will reflect less light back to the viewer. +// This implementation is based on [1] Equation 4, and we adopt their modifications to +// alphaRoughness as input as originally proposed in [2]. +float geometricOcclusion(PBRInfo pbrInputs) +{ + float NdotL = pbrInputs.NdotL; + float NdotV = pbrInputs.NdotV; + float r = pbrInputs.alphaRoughness; + + float attenuationL = 2.0 * NdotL / (NdotL + sqrt(r * r + (1.0 - r * r) * (NdotL * NdotL))); + float attenuationV = 2.0 * NdotV / (NdotV + sqrt(r * r + (1.0 - r * r) * (NdotV * NdotV))); + return attenuationL * attenuationV; +} + +// The following equation(s) model the distribution of microfacet normals across the area being drawn (aka D()) +// Implementation from "Average Irregularity Representation of a Roughened Surface for Ray Reflection" by T. S. Trowbridge, and K. P. Reitz +// Follows the distribution function recommended in the SIGGRAPH 2013 course notes from EPIC Games [1], Equation 3. +float microfacetDistribution(PBRInfo pbrInputs) +{ + float roughnessSq = pbrInputs.alphaRoughness * pbrInputs.alphaRoughness; + float f = (pbrInputs.NdotH * roughnessSq - pbrInputs.NdotH) * pbrInputs.NdotH + 1.0; + return roughnessSq / (M_PI * f * f); +} + +// Gets metallic factor from specular glossiness workflow inputs +float convertMetallic(vec3 diffuse, vec3 specular, float maxSpecular) { + float perceivedDiffuse = sqrt(0.299 * diffuse.r * diffuse.r + 0.587 * diffuse.g * diffuse.g + 0.114 * diffuse.b * diffuse.b); + float perceivedSpecular = sqrt(0.299 * specular.r * specular.r + 0.587 * specular.g * specular.g + 0.114 * specular.b * specular.b); + if (perceivedSpecular < c_MinRoughness) { + return 0.0; + } + float a = c_MinRoughness; + float b = perceivedDiffuse * (1.0 - maxSpecular) / (1.0 - c_MinRoughness) + perceivedSpecular - 2.0 * c_MinRoughness; + float c = c_MinRoughness - perceivedSpecular; + float D = max(b * b - 4.0 * a * c, 0.0); + return clamp((-b + sqrt(D)) / (2.0 * a), 0.0, 1.0); +} + +void main() +{ + if (subpassLoad(samplerPos, gl_SampleID).a == 1.0f) + discard; + + float perceptualRoughness = subpassLoad(samplerColorRough, gl_SampleID).a; + float metallic = subpassLoad(samplerEmitMetal, gl_SampleID).a; + vec3 diffuseColor; + vec4 baseColor = vec4(subpassLoad(samplerColorRough, gl_SampleID).rgb, 1); + vec3 emissive = subpassLoad (samplerEmitMetal, gl_SampleID).rgb; + + vec3 f0 = vec3(0.04); + + diffuseColor = baseColor.rgb * (vec3(1.0) - f0); + diffuseColor *= 1.0 - metallic; + + float alphaRoughness = perceptualRoughness * perceptualRoughness; + + vec3 specularColor = mix(f0, baseColor.rgb, metallic); + + // Compute reflectance. + float reflectance = max(max(specularColor.r, specularColor.g), specularColor.b); + + // For typical incident reflectance range (between 4% to 100%) set the grazing reflectance to 100% for typical fresnel effect. + // For very low reflectance range on highly diffuse objects (below 4%), incrementally reduce grazing reflecance to 0%. + float reflectance90 = clamp(reflectance * 25.0, 0.0, 1.0); + vec3 specularEnvironmentR0 = specularColor.rgb; + vec3 specularEnvironmentR90 = vec3(1.0) * reflectance90; + + vec3 n = subpassLoad(samplerN_AO, gl_SampleID).rgb; + vec3 pos = subpassLoad(samplerPos, gl_SampleID).rgb; + vec3 v = normalize(ubo.camPos.xyz - pos); // Vector from surface point to camera + + vec3 colors = vec3(0); + vec3 lightTarget = vec3(0); + + for (int i=0; i -1.0 && shadowCoord.z < 1.0) + { + float dist = texture(samplerShadowMap, vec3(shadowCoord.st + offset, layer)).r; + if (shadowCoord.w > 0.0 && dist < shadowCoord.z) + shadow = SHADOW_FACTOR; + }else + shadow = 0.05f;//for debug view out of light proj + + return shadow; +} + +float filterPCF(vec4 sc, float layer) +{ + ivec2 texDim = textureSize(samplerShadowMap, 0).xy; + float scale = 1.5; + float dx = scale * 1.0 / float(texDim.x); + float dy = scale * 1.0 / float(texDim.y); + + float shadowFactor = 0.0; + int count = 0; + int range = 1; + + for (int x = -range; x <= range; x++) + { + for (int y = -range; y <= range; y++) + { + shadowFactor += textureProj(sc, layer, vec2(dx*x, dy*y)); + count++; + } + + } + return shadowFactor / count; +} + +void main() +{ + if (subpassLoad(samplerPos, gl_SampleID).a == 1.0f) + discard; + + float perceptualRoughness = subpassLoad(samplerColorRough, gl_SampleID).a; + float metallic = subpassLoad(samplerEmitMetal, gl_SampleID).a; + vec3 diffuseColor; + vec4 baseColor = vec4(subpassLoad(samplerColorRough, gl_SampleID).rgb, 1); + vec3 emissive = subpassLoad (samplerEmitMetal, gl_SampleID).rgb; + + vec3 f0 = vec3(0.04); + + diffuseColor = baseColor.rgb * (vec3(1.0) - f0); + diffuseColor *= 1.0 - metallic; + + float alphaRoughness = perceptualRoughness * perceptualRoughness; + + vec3 specularColor = mix(f0, baseColor.rgb, metallic); + + // Compute reflectance. + float reflectance = max(max(specularColor.r, specularColor.g), specularColor.b); + + // For typical incident reflectance range (between 4% to 100%) set the grazing reflectance to 100% for typical fresnel effect. + // For very low reflectance range on highly diffuse objects (below 4%), incrementally reduce grazing reflecance to 0%. + float reflectance90 = clamp(reflectance * 25.0, 0.0, 1.0); + vec3 specularEnvironmentR0 = specularColor.rgb; + vec3 specularEnvironmentR90 = vec3(1.0) * reflectance90; + + vec3 n = subpassLoad(samplerN_AO, gl_SampleID).rgb; + vec3 pos = subpassLoad(samplerPos, gl_SampleID).rgb; + vec3 v = normalize(ubo.camPos.xyz - pos); // Vector from surface point to camera + + vec3 colors = vec3(0); + vec3 lightTarget = vec3(0); + + for (int i=0; i> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + float rdi = float(bits) * 2.3283064365386963e-10; + return vec2(float(i) /float(N), rdi); +} + +// Based on http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_slides.pdf +vec3 importanceSample_GGX(vec2 Xi, float roughness, vec3 normal) +{ + // Maps a 2D point to a hemisphere with spread based on roughness + float alpha = roughness * roughness; + float phi = 2.0 * PI * Xi.x + random(normal.xz) * 0.1; + float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (alpha*alpha - 1.0) * Xi.y)); + float sinTheta = sqrt(1.0 - cosTheta * cosTheta); + vec3 H = vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta); + + // Tangent space + vec3 up = abs(normal.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); + vec3 tangentX = normalize(cross(up, normal)); + vec3 tangentY = normalize(cross(normal, tangentX)); + + // Convert to world Space + return normalize(tangentX * H.x + tangentY * H.y + normal * H.z); +} + +// Geometric Shadowing function +float G_SchlicksmithGGX(float dotNL, float dotNV, float roughness) +{ + float k = (roughness * roughness) / 2.0; + float GL = dotNL / (dotNL * (1.0 - k) + k); + float GV = dotNV / (dotNV * (1.0 - k) + k); + return GL * GV; +} + +vec2 BRDF(float NoV, float roughness) +{ + // Normal always points along z-axis for the 2D lookup + const vec3 N = vec3(0.0, 0.0, 1.0); + vec3 V = vec3(sqrt(1.0 - NoV*NoV), 0.0, NoV); + + vec2 LUT = vec2(0.0); + for(uint i = 0u; i < NUM_SAMPLES; i++) { + vec2 Xi = hammersley2d(i, NUM_SAMPLES); + vec3 H = importanceSample_GGX(Xi, roughness, N); + vec3 L = 2.0 * dot(V, H) * H - V; + + float dotNL = max(dot(N, L), 0.0); + float dotNV = max(dot(N, V), 0.0); + float dotVH = max(dot(V, H), 0.0); + float dotNH = max(dot(H, N), 0.0); + + if (dotNL > 0.0) { + float G = G_SchlicksmithGGX(dotNL, dotNV, roughness); + float G_Vis = (G * dotVH) / (dotNH * dotNV); + float Fc = pow(1.0 - dotVH, 5.0); + LUT += vec2((1.0 - Fc) * G_Vis, Fc * G_Vis); + } + } + return LUT / float(NUM_SAMPLES); +} + +void main() +{ + outColor = vec4(BRDF(inUV.s, 1.0-inUV.t), 0.0, 1.0); +} \ No newline at end of file diff --git a/samples/deferred/shaders/genbrdflut.vert b/samples/deferred/shaders/genbrdflut.vert new file mode 100644 index 0000000..f3dd233 --- /dev/null +++ b/samples/deferred/shaders/genbrdflut.vert @@ -0,0 +1,9 @@ +#version 450 + +layout (location = 0) out vec2 outUV; + +void main() +{ + outUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); + gl_Position = vec4(outUV * 2.0f - 1.0f, 0.0f, 1.0f); +} \ No newline at end of file diff --git a/samples/deferred/shaders/irradiancecube.frag b/samples/deferred/shaders/irradiancecube.frag new file mode 100644 index 0000000..340d679 --- /dev/null +++ b/samples/deferred/shaders/irradiancecube.frag @@ -0,0 +1,37 @@ +// Generates an irradiance cube from an environment map using convolution + +#version 450 + +layout (location = 0) in vec3 inPos; +layout (location = 0) out vec4 outColor; +layout (binding = 0) uniform samplerCube samplerEnv; + +layout(push_constant) uniform PushConsts { + layout (offset = 64) float deltaPhi; + layout (offset = 68) float deltaTheta; +} consts; + +#define PI 3.1415926535897932384626433832795 + +void main() +{ + vec3 N = normalize(inPos); + vec3 up = vec3(0.0, 1.0, 0.0); + vec3 right = normalize(cross(up, N)); + up = cross(N, right); + + const float TWO_PI = PI * 2.0; + const float HALF_PI = PI * 0.5; + + vec3 color = vec3(0.0); + uint sampleCount = 0u; + for (float phi = 0.0; phi < TWO_PI; phi += consts.deltaPhi) { + for (float theta = 0.0; theta < HALF_PI; theta += consts.deltaTheta) { + vec3 tempVec = cos(phi) * right + sin(phi) * up; + vec3 sampleVector = cos(theta) * N + sin(theta) * tempVec; + color += texture(samplerEnv, sampleVector).rgb * cos(theta) * sin(theta); + sampleCount++; + } + } + outColor = vec4(PI * color / float(sampleCount), 1.0);//texture(samplerEnv, inPos).rgba; +} diff --git a/samples/deferred/shaders/prefilterenvmap.frag b/samples/deferred/shaders/prefilterenvmap.frag new file mode 100644 index 0000000..ae1212e --- /dev/null +++ b/samples/deferred/shaders/prefilterenvmap.frag @@ -0,0 +1,105 @@ +#version 450 + +layout (location = 0) in vec3 inPos; +layout (location = 0) out vec4 outColor; + +layout (binding = 0) uniform samplerCube samplerEnv; + +layout(push_constant) uniform PushConsts { + layout (offset = 64) float roughness; + layout (offset = 68) uint numSamples; +} consts; + +const float PI = 3.1415926536; + +// Based omn http://byteblacksmith.com/improvements-to-the-canonical-one-liner-glsl-rand-for-opengl-es-2-0/ +float random(vec2 co) +{ + float a = 12.9898; + float b = 78.233; + float c = 43758.5453; + float dt= dot(co.xy ,vec2(a,b)); + float sn= mod(dt,3.14); + return fract(sin(sn) * c); +} + +vec2 hammersley2d(uint i, uint N) +{ + // Radical inverse based on http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html + uint bits = (i << 16u) | (i >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + float rdi = float(bits) * 2.3283064365386963e-10; + return vec2(float(i) /float(N), rdi); +} + +// Based on http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_slides.pdf +vec3 importanceSample_GGX(vec2 Xi, float roughness, vec3 normal) +{ + // Maps a 2D point to a hemisphere with spread based on roughness + float alpha = roughness * roughness; + float phi = 2.0 * PI * Xi.x + random(normal.xz) * 0.1; + float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (alpha*alpha - 1.0) * Xi.y)); + float sinTheta = sqrt(1.0 - cosTheta * cosTheta); + vec3 H = vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta); + + // Tangent space + vec3 up = abs(normal.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); + vec3 tangentX = normalize(cross(up, normal)); + vec3 tangentY = normalize(cross(normal, tangentX)); + + // Convert to world Space + return normalize(tangentX * H.x + tangentY * H.y + normal * H.z); +} + +// Normal Distribution function +float D_GGX(float dotNH, float roughness) +{ + float alpha = roughness * roughness; + float alpha2 = alpha * alpha; + float denom = dotNH * dotNH * (alpha2 - 1.0) + 1.0; + return (alpha2)/(PI * denom*denom); +} + +vec3 prefilterEnvMap(vec3 R, float roughness) +{ + vec3 N = R; + vec3 V = R; + vec3 color = vec3(0.0); + float totalWeight = 0.0; + float envMapDim = float(textureSize(samplerEnv, 0).s); + for(uint i = 0u; i < consts.numSamples; i++) { + vec2 Xi = hammersley2d(i, consts.numSamples); + vec3 H = importanceSample_GGX(Xi, roughness, N); + vec3 L = 2.0 * dot(V, H) * H - V; + float dotNL = clamp(dot(N, L), 0.0, 1.0); + if(dotNL > 0.0) { + // Filtering based on https://placeholderart.wordpress.com/2015/07/28/implementation-notes-runtime-environment-map-filtering-for-image-based-lighting/ + + float dotNH = clamp(dot(N, H), 0.0, 1.0); + float dotVH = clamp(dot(V, H), 0.0, 1.0); + + // Probability Distribution Function + float pdf = D_GGX(dotNH, roughness) * dotNH / (4.0 * dotVH) + 0.0001; + // Slid angle of current smple + float omegaS = 1.0 / (float(consts.numSamples) * pdf); + // Solid angle of 1 pixel across all cube faces + float omegaP = 4.0 * PI / (6.0 * envMapDim * envMapDim); + // Biased (+1.0) mip level for better result + float mipLevel = roughness == 0.0 ? 0.0 : max(0.5 * log2(omegaS / omegaP) + 1.0, 0.0f); + color += textureLod(samplerEnv, L, mipLevel).rgb * dotNL; + totalWeight += dotNL; + + } + } + return (color / totalWeight); +} + + +void main() +{ + vec3 N = normalize(inPos); + outColor = vec4(prefilterEnvMap(N, consts.roughness), 1.0); +} diff --git a/samples/deferred/shaders/shadow.geom b/samples/deferred/shaders/shadow.geom new file mode 100644 index 0000000..9fd322c --- /dev/null +++ b/samples/deferred/shaders/shadow.geom @@ -0,0 +1,34 @@ +#version 420 + +#define NUM_LIGHTS 2 + +layout (triangles, invocations = NUM_LIGHTS) in; +layout (triangle_strip, max_vertices = 3) out; + +struct Light { + vec4 position; + vec4 color; + mat4 mvp; +}; +layout (set = 0, binding = 0) uniform UBO { + mat4 projection; + mat4 model; + mat4 view; +}; +layout (set = 0, binding = 1) uniform UBOLights { + Light lights[NUM_LIGHTS]; +}; + +//layout (location = 0) in int inInstanceIndex[]; + + +void main() +{ + for (int i = 0; i < gl_in.length(); i++) + { + gl_Layer = gl_InvocationID; + gl_Position = lights[gl_InvocationID].mvp * model * gl_in[i].gl_Position; + EmitVertex(); + } + EndPrimitive(); +} \ No newline at end of file diff --git a/samples/deferred/shaders/shadow.vert b/samples/deferred/shaders/shadow.vert new file mode 100644 index 0000000..b0bd6c0 --- /dev/null +++ b/samples/deferred/shaders/shadow.vert @@ -0,0 +1,12 @@ +#version 450 + +layout (location = 0) in vec3 inPos; + +layout(push_constant) uniform PushConsts { + mat4 model; +} pc; + +void main() +{ + gl_Position = pc.model * vec4(inPos,1); +} \ No newline at end of file diff --git a/samples/deferred/shaders/show_gbuff.frag b/samples/deferred/shaders/show_gbuff.frag new file mode 100644 index 0000000..7859e36 --- /dev/null +++ b/samples/deferred/shaders/show_gbuff.frag @@ -0,0 +1,92 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (input_attachment_index = 0, set = 1, binding = 0) uniform subpassInputMS samplerColorRough; +layout (input_attachment_index = 1, set = 1, binding = 1) uniform subpassInputMS samplerEmitMetal; +layout (input_attachment_index = 2, set = 1, binding = 2) uniform subpassInputMS samplerN_AO; +layout (input_attachment_index = 3, set = 1, binding = 3) uniform subpassInputMS samplerPos; + +layout (set = 0, binding = 1) uniform samplerCube samplerIrradiance; +layout (set = 0, binding = 2) uniform samplerCube prefilteredMap; +layout (set = 0, binding = 3) uniform sampler2D samplerBRDFLUT; +layout (set = 0, binding = 6) uniform sampler2DArray samplerShadowMap; + +layout (push_constant) uniform PushCsts { + layout(offset = 64) + int imgIdx; +}; + +layout (location = 0) in vec2 inUV; +layout (location = 0) out vec4 outColor; + +const uint color = 0; +const uint normal = 1; +const uint pos = 2; +const uint occlusion = 3; +const uint emissive = 4; +const uint metallic = 5; +const uint roughness = 6; +const uint depth = 7; +const uint prefill = 8; +const uint irradiance = 9; +const uint shadowMap = 10; + +vec4 sampleCubeMap (samplerCube sc, uint face, uint lod) { + vec2 uv = 2.0 * inUV - vec2(1.0); + switch (face) { + case 0: + return vec4 (textureLod (sc, vec3(1, uv.t, uv.s), lod).rgb, 1); + case 1: + return vec4 (textureLod (sc, vec3(-1, uv.t, uv.s), lod).rgb, 1); + case 2: + return vec4 (textureLod (sc, vec3(uv.s, 1, -uv.t), lod).rgb, 1); + case 3: + return vec4 (textureLod (sc, vec3(uv.s, -1, uv.t), lod).rgb, 1); + case 4: + return vec4 (textureLod (sc, vec3(uv, 1), lod).rgb, 1); + case 5: + return vec4 (textureLod (sc, vec3(-uv.s, uv.t, -1), lod).rgb, 1); + } +} + +void main() +{ + uint imgNum = bitfieldExtract (imgIdx, 0, 8); + switch (imgNum) { + case color: + outColor = vec4(subpassLoad(samplerColorRough, gl_SampleID).rgb, 1); + break; + case normal: + outColor = vec4(subpassLoad(samplerN_AO, gl_SampleID).rgb, 1); + break; + case pos: + outColor = vec4(subpassLoad(samplerPos, gl_SampleID).rgb, 1); + break; + case occlusion: + outColor = vec4(subpassLoad(samplerN_AO, gl_SampleID).aaa, 1); + break; + case emissive: + outColor = vec4(subpassLoad(samplerEmitMetal, gl_SampleID).rgb, 1); + break; + case metallic: + outColor = vec4(subpassLoad(samplerEmitMetal, gl_SampleID).aaa, 1); + break; + case roughness: + outColor = vec4(subpassLoad(samplerColorRough, gl_SampleID).aaa, 1); + break; + case depth: + outColor = vec4(subpassLoad(samplerPos, gl_SampleID).aaa, 1); + break; + case shadowMap: + vec3 d = texture(samplerShadowMap, vec3(inUV, bitfieldExtract (imgIdx, 8, 8))).rrr; + outColor = vec4(d*d*d, 1); + break; + default: + if (imgNum == prefill) + outColor = sampleCubeMap (prefilteredMap, bitfieldExtract (imgIdx, 8, 8), bitfieldExtract (imgIdx, 16, 8)); + else if (imgNum == irradiance) + outColor = sampleCubeMap (samplerIrradiance, bitfieldExtract (imgIdx, 8, 8), bitfieldExtract (imgIdx, 16, 8)); + break; + } +} \ No newline at end of file diff --git a/samples/deferred/shaders/simpletexture.frag b/samples/deferred/shaders/simpletexture.frag new file mode 100644 index 0000000..a081876 --- /dev/null +++ b/samples/deferred/shaders/simpletexture.frag @@ -0,0 +1,15 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (set = 0, binding = 0) uniform sampler2D samplerColor; + +layout (location = 0) in vec2 inUV; + +layout (location = 0) out vec4 outFragColor; + +void main() +{ + outFragColor = texture(samplerColor, inUV); +} diff --git a/samples/deferred/shaders/skybox.frag b/samples/deferred/shaders/skybox.frag new file mode 100644 index 0000000..e505ef1 --- /dev/null +++ b/samples/deferred/shaders/skybox.frag @@ -0,0 +1,12 @@ +#version 450 + +layout (binding = 2) uniform samplerCube samplerEnv; + +layout (set = 0, location = 0) in vec3 inUVW; + +layout (set = 0, location = 0) out vec4 outColor; + +void main() +{ + outColor = vec4(textureLod(samplerEnv, inUVW, 1.5).rgb, 1.0); +} \ No newline at end of file diff --git a/samples/deferred/shaders/skybox.vert b/samples/deferred/shaders/skybox.vert new file mode 100644 index 0000000..22e285e --- /dev/null +++ b/samples/deferred/shaders/skybox.vert @@ -0,0 +1,27 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout (location = 0) in vec3 inPos; + +layout (binding = 0) uniform UBO +{ + mat4 projection; + mat4 model; + mat4 view; +} ubo; + +layout (location = 0) out vec3 outUVW; + +out gl_PerVertex +{ + vec4 gl_Position; +}; + +void main() +{ + outUVW = inPos; + outUVW.y = -outUVW.y; + gl_Position = ubo.projection * mat4(mat3(ubo.view)) * vec4(inPos, 1.0); +} diff --git a/samples/deferred/shaders/tone_mapping.frag b/samples/deferred/shaders/tone_mapping.frag new file mode 100644 index 0000000..6a1a8d4 --- /dev/null +++ b/samples/deferred/shaders/tone_mapping.frag @@ -0,0 +1,70 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout(push_constant) uniform PushConsts { + float exposure; + float gamma; + float debug; +} pc; + +layout (set = 0, binding = 0) uniform sampler2D samplerHDR; +layout (set = 0, binding = 1) uniform sampler2D bloom; + +layout (location = 0) in vec2 inUV; +layout (location = 0) out vec4 outColor; + +vec3 Uncharted2Tonemap(vec3 color) +{ + const float A = 0.15; + const float B = 0.50; + const float C = 0.10; + const float D = 0.20; + const float E = 0.02; + const float F = 0.30; + const float W = 11.2; + return ((color*(A*color+C*B)+D*E)/(color*(A*color+B)+D*F))-E/F; +} + +vec3 tonemap(vec3 color) +{ + vec3 outcol = Uncharted2Tonemap(color.rgb * pc.exposure); + outcol = outcol * (1.0f / Uncharted2Tonemap(vec3(11.2f))); + return pow(outcol, vec3(1.0f / pc.gamma)); +} + +vec3 SRGBtoLINEAR(vec3 srgbIn) +{ + #ifdef MANUAL_SRGB + #ifdef SRGB_FAST_APPROXIMATION + return pow(srgbIn.xyz,vec3(2.2)); + #else //SRGB_FAST_APPROXIMATION + vec3 bLess = step(vec3(0.04045),srgbIn.xyz); + return linOut = mix( srgbIn.xyz/vec3(12.92), pow((srgbIn.xyz+vec3(0.055))/vec3(1.055),vec3(2.4)), bLess ); + #endif //SRGB_FAST_APPROXIMATION + #else //MANUAL_SRGB + return srgbIn; + #endif //MANUAL_SRGB +} + +void main() +{ + if (pc.debug < 0.0f) { + //vec4 hdrColor = texelFetch (samplerHDR, ivec2(inUV), gl_SampleID); + vec4 hdrColor = texture (samplerHDR, inUV); + //vec4 c = texture (bloom, inUV); + //float lum = (0.299*c.r + 0.587*c.g + 0.114*c.b); + //if (lum>1.0) + // hdrColor.rgb += c.rgb * 0.05; + outColor = vec4(SRGBtoLINEAR(tonemap(hdrColor.rgb)), hdrColor.a); + }else + outColor = texture (bloom, inUV); + + + /* + outColor = vec4(SRGBtoLINEAR(tonemap(hdrColor.rgb)), hdrColor.a);;*/ + +/* vec3 mapped = vec3(1.0) - exp(-hdrColor.rgb * pc.exposure); + mapped = pow(mapped, vec3(1.0 / pc.gamma)); + outColor = vec4(mapped, hdrColor.a);*/ +} \ No newline at end of file diff --git a/samples/deferred/shadowMapRenderer.cs b/samples/deferred/shadowMapRenderer.cs new file mode 100644 index 0000000..a4684ec --- /dev/null +++ b/samples/deferred/shadowMapRenderer.cs @@ -0,0 +1,157 @@ +/*shadow mapping greatly inspired from: +* Vulkan Example - Deferred shading with shadows from multiple light sources using geometry shader instancing +* Copyright (C) 2016 by Sascha Willems - www.saschawillems.de +* This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) +*/ +using System; +using System.Numerics; +using System.Runtime.InteropServices; +using CVKL; +using CVKL.glTF; +using VK; +using static deferred.DeferredPbrRenderer; + +namespace deferred { + public class ShadowMapRenderer : IDisposable { + Device dev; + Queue gQueue; + + public static uint SHADOWMAP_SIZE = 4096; + public static VkFormat SHADOWMAP_FORMAT = VkFormat.D32SfloatS8Uint; + public static VkSampleCountFlags SHADOWMAP_NUM_SAMPLES = VkSampleCountFlags.SampleCount1; + public bool updateShadowMap = true; + + public float depthBiasConstant = 1.5f; + public float depthBiasSlope = 1.75f; + float lightFOV = Utils.DegreesToRadians (60); + float lightFarPlane; + + + + RenderPass shadowPass; + Framebuffer fbShadowMap; + public Image shadowMap { get; private set; } + Pipeline shadowPipeline; + DescriptorPool descriptorPool; + DescriptorSetLayout descLayoutShadow; + DescriptorSet dsShadow; + DeferredPbrRenderer renderer; + + public ShadowMapRenderer (Queue gQueue, DeferredPbrRenderer renderer, float farPlane = 16f) { + this.lightFarPlane = farPlane; + this.gQueue = gQueue; + this.dev = gQueue.Dev; + this.renderer = renderer; + + descriptorPool = new DescriptorPool (dev, 1, + new VkDescriptorPoolSize (VkDescriptorType.UniformBuffer, 2) + ); + + init (); + } + + void init () { + + //Shadow map renderpass + shadowPass = new RenderPass (dev, VkSampleCountFlags.SampleCount1); + shadowPass.AddAttachment (SHADOWMAP_FORMAT, VkImageLayout.DepthStencilReadOnlyOptimal, SHADOWMAP_NUM_SAMPLES); + shadowPass.ClearValues.Add (new VkClearValue { depthStencil = new VkClearDepthStencilValue (1.0f, 0) }); + + SubPass subpass0 = new SubPass (); + subpass0.SetDepthReference (0); + shadowPass.AddSubpass (subpass0); + + shadowPass.AddDependency (Vk.SubpassExternal, 0, + VkPipelineStageFlags.FragmentShader, VkPipelineStageFlags.EarlyFragmentTests, + VkAccessFlags.ShaderRead, VkAccessFlags.DepthStencilAttachmentWrite); + shadowPass.AddDependency (0, Vk.SubpassExternal, + VkPipelineStageFlags.LateFragmentTests, VkPipelineStageFlags.FragmentShader, + VkAccessFlags.DepthStencilAttachmentWrite, VkAccessFlags.ShaderRead); + + descLayoutShadow = new DescriptorSetLayout (dev, + new VkDescriptorSetLayoutBinding (0, VkShaderStageFlags.Geometry, VkDescriptorType.UniformBuffer),//matrices + new VkDescriptorSetLayoutBinding (1, VkShaderStageFlags.Geometry, VkDescriptorType.UniformBuffer));//lights + + GraphicPipelineConfig cfg = GraphicPipelineConfig.CreateDefault (VkPrimitiveTopology.TriangleList); + cfg.rasterizationState.cullMode = VkCullModeFlags.Back; + cfg.rasterizationState.depthBiasEnable = true; + cfg.dynamicStates.Add (VkDynamicState.DepthBias); + + cfg.RenderPass = shadowPass; + + cfg.Layout = new PipelineLayout (dev, descLayoutShadow); + cfg.Layout.AddPushConstants ( + new VkPushConstantRange (VkShaderStageFlags.Vertex|VkShaderStageFlags.Geometry, (uint)Marshal.SizeOf ()) + ); + + cfg.AddVertexBinding (0); + cfg.AddVertexAttributes (0, VkFormat.R32g32b32Sfloat); + + cfg.AddShader (VkShaderStageFlags.Vertex, "#deferred.shadow.vert.spv"); + cfg.AddShader (VkShaderStageFlags.Geometry, "#deferred.shadow.geom.spv"); + + shadowPipeline = new GraphicPipeline (cfg); + + //shadow map image + shadowMap = new Image (dev, SHADOWMAP_FORMAT, VkImageUsageFlags.DepthStencilAttachment | VkImageUsageFlags.Sampled, VkMemoryPropertyFlags.DeviceLocal, SHADOWMAP_SIZE, SHADOWMAP_SIZE, + VkImageType.Image2D, SHADOWMAP_NUM_SAMPLES, VkImageTiling.Optimal, 1, (uint)renderer.lights.Length); + shadowMap.CreateView (VkImageViewType.ImageView2DArray, VkImageAspectFlags.Depth, shadowMap.CreateInfo.arrayLayers); + shadowMap.CreateSampler (VkSamplerAddressMode.ClampToBorder); + shadowMap.Descriptor.imageLayout = VkImageLayout.DepthStencilReadOnlyOptimal; + + fbShadowMap = new Framebuffer (shadowPass, SHADOWMAP_SIZE, SHADOWMAP_SIZE, (uint)renderer.lights.Length); + fbShadowMap.attachments.Add (shadowMap); + fbShadowMap.Activate (); + + dsShadow = descriptorPool.Allocate (descLayoutShadow); + + DescriptorSetWrites dsWrite = new DescriptorSetWrites (dsShadow, descLayoutShadow); + dsWrite.Write (dev, renderer.uboMatrices.Descriptor, renderer.uboLights.Descriptor); + } + + public void update_light_matrices () { + Matrix4x4 proj = Matrix4x4.CreatePerspectiveFieldOfView (lightFOV, 1, 0.1f, lightFarPlane); + for (int i = 0; i < renderer.lights.Length; i++) { + Matrix4x4 view = Matrix4x4.CreateLookAt (renderer.lights[i].position.ToVector3 (), Vector3.Zero, Vector3.UnitY); + renderer.lights[i].mvp = view * proj; + } + renderer.uboLights.Update (renderer.lights); + dev.WaitIdle (); + } + + public void update_shadow_map (CommandPool cmdPool) { + update_light_matrices (); + + CommandBuffer cmd = cmdPool.AllocateAndStart (); + + shadowPass.Begin (cmd, fbShadowMap); + + cmd.SetViewport (SHADOWMAP_SIZE, SHADOWMAP_SIZE); + cmd.SetScissor (SHADOWMAP_SIZE, SHADOWMAP_SIZE); + + cmd.BindDescriptorSet (shadowPipeline.Layout, dsShadow); + + Vk.vkCmdSetDepthBias (cmd.Handle, depthBiasConstant, 0.0f, depthBiasSlope); + + shadowPipeline.Bind (cmd); + + if (renderer.model != null) { + renderer.model.Bind (cmd); + renderer.model.DrawAll (cmd, shadowPipeline.Layout, true); + } + + shadowPass.End (cmd); + + gQueue.EndSubmitAndWait (cmd); + updateShadowMap = false; + } + + + public void Dispose () { + shadowPipeline?.Dispose (); + fbShadowMap?.Dispose (); + shadowMap?.Dispose (); + descriptorPool?.Dispose (); + } + } +} diff --git a/samples/deferred/ui/debug.crow b/samples/deferred/ui/debug.crow new file mode 100644 index 0000000..4d78bf1 --- /dev/null +++ b/samples/deferred/ui/debug.crow @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/samples/deferred/ui/deferred.style b/samples/deferred/ui/deferred.style new file mode 100644 index 0000000..c21ffb6 --- /dev/null +++ b/samples/deferred/ui/deferred.style @@ -0,0 +1,61 @@ +icon { + Width="14"; + Height="14"; +} +MemberViewLabel { + Margin="1"; + Height="Fit"; + Width="50%"; + Background="White"; +} +MemberViewHStack { + Focusable="true"; + Height="Fit"; + Spacing="1"; + MouseEnter="{Background=SteelBlue}"; + MouseLeave="{Background=Transparent}"; +} + +IcoBut { + Template = "#Crow.Coding.ui.IcoBut.template"; + MinimumSize = "10,10"; + Width = "8"; + Height = "14"; + Background = "White"; +} + +WinSchema { + Template="#Crow.Coding.ui.DockWindows.WinSchemaItem.template"; + Focusable = "true"; + MinimumSize="150,50"; + Width = "Fit"; + Height = "Fit"; +} +TabItem { + AllowDrag="false"; + Background="Jet"; +} +TreeItemBorder { + CornerRadius="2"; + Margin="0"; + Focusable="true"; + Height="Fit"; + Width="Stretched"; + Foreground="Transparent"; + MouseEnter="{Foreground=DimGrey}"; + MouseLeave="{Foreground=Transparent}"; +} +TreeIcon { + Margin="0"; + Width="14"; + Height="14"; +} +NormalizedFloat { + Minimum="0.0"; + Maximum="1.0"; + SmallIncrement="0.01"; + LargeIncrement="0.1"; + Decimals="2"; + Width="60"; + Foreground="Black"; +} diff --git a/samples/deferred/ui/main.crow b/samples/deferred/ui/main.crow new file mode 100644 index 0000000..14e6082 --- /dev/null +++ b/samples/deferred/ui/main.crow @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/deferred/ui/materials.crow b/samples/deferred/ui/materials.crow new file mode 100644 index 0000000..b4c5432 --- /dev/null +++ b/samples/deferred/ui/materials.crow @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + +