From af98b8751b9d78d23924f8fe12b5749220728098 Mon Sep 17 00:00:00 2001 From: Sergey Kosarevsky <sk@linderdaum.com> Date: Mon, 18 Nov 2024 19:24:05 -0800 Subject: [PATCH] Added 'Chapter10' --- CMakeLists.txt | 9 + Chapter08/VKMesh08.h | 15 +- .../01_OffscreenRendering/CMakeLists.txt | 9 + Chapter10/01_OffscreenRendering/src/main.cpp | 190 +++++++ Chapter10/01_OffscreenRendering/src/main.frag | 14 + Chapter10/01_OffscreenRendering/src/main.vert | 22 + Chapter10/02_ShadowMapping/CMakeLists.txt | 9 + Chapter10/02_ShadowMapping/src/common.sp | 25 + Chapter10/02_ShadowMapping/src/main.cpp | 346 ++++++++++++ Chapter10/02_ShadowMapping/src/main.frag | 53 ++ Chapter10/02_ShadowMapping/src/main.vert | 20 + Chapter10/02_ShadowMapping/src/shadow.frag | 4 + Chapter10/02_ShadowMapping/src/shadow.vert | 10 + Chapter10/03_MSAA/CMakeLists.txt | 9 + Chapter10/03_MSAA/src/main.cpp | 122 +++++ Chapter10/04_SSAO/CMakeLists.txt | 9 + Chapter10/04_SSAO/src/SSAO.comp | 70 +++ Chapter10/04_SSAO/src/combine.frag | 23 + Chapter10/04_SSAO/src/combine.vert | 9 + Chapter10/04_SSAO/src/main.cpp | 317 +++++++++++ Chapter10/05_HDR/CMakeLists.txt | 9 + Chapter10/05_HDR/src/Bloom.comp | 68 +++ Chapter10/05_HDR/src/BrightPass.comp | 54 ++ Chapter10/05_HDR/src/ToneMap.frag | 113 ++++ Chapter10/05_HDR/src/main.cpp | 445 ++++++++++++++++ Chapter10/06_HDR_Adaptation/CMakeLists.txt | 9 + .../06_HDR_Adaptation/src/Adaptation.comp | 21 + Chapter10/06_HDR_Adaptation/src/main.cpp | 495 ++++++++++++++++++ Chapter10/Bistro.h | 78 +++ Chapter10/Skybox.h | 48 ++ deps/bootstrap.json | 2 +- 31 files changed, 2619 insertions(+), 8 deletions(-) create mode 100644 Chapter10/01_OffscreenRendering/CMakeLists.txt create mode 100644 Chapter10/01_OffscreenRendering/src/main.cpp create mode 100644 Chapter10/01_OffscreenRendering/src/main.frag create mode 100644 Chapter10/01_OffscreenRendering/src/main.vert create mode 100644 Chapter10/02_ShadowMapping/CMakeLists.txt create mode 100644 Chapter10/02_ShadowMapping/src/common.sp create mode 100644 Chapter10/02_ShadowMapping/src/main.cpp create mode 100644 Chapter10/02_ShadowMapping/src/main.frag create mode 100644 Chapter10/02_ShadowMapping/src/main.vert create mode 100644 Chapter10/02_ShadowMapping/src/shadow.frag create mode 100644 Chapter10/02_ShadowMapping/src/shadow.vert create mode 100644 Chapter10/03_MSAA/CMakeLists.txt create mode 100644 Chapter10/03_MSAA/src/main.cpp create mode 100644 Chapter10/04_SSAO/CMakeLists.txt create mode 100644 Chapter10/04_SSAO/src/SSAO.comp create mode 100644 Chapter10/04_SSAO/src/combine.frag create mode 100644 Chapter10/04_SSAO/src/combine.vert create mode 100644 Chapter10/04_SSAO/src/main.cpp create mode 100644 Chapter10/05_HDR/CMakeLists.txt create mode 100644 Chapter10/05_HDR/src/Bloom.comp create mode 100644 Chapter10/05_HDR/src/BrightPass.comp create mode 100644 Chapter10/05_HDR/src/ToneMap.frag create mode 100644 Chapter10/05_HDR/src/main.cpp create mode 100644 Chapter10/06_HDR_Adaptation/CMakeLists.txt create mode 100644 Chapter10/06_HDR_Adaptation/src/Adaptation.comp create mode 100644 Chapter10/06_HDR_Adaptation/src/main.cpp create mode 100644 Chapter10/Bistro.h create mode 100644 Chapter10/Skybox.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9eab1b2..0c16808 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -168,3 +168,12 @@ add_subdirectory(Chapter09/01_AnimationPlayer) add_subdirectory(Chapter09/02_Skinning) add_subdirectory(Chapter09/03_Morphing) add_subdirectory(Chapter09/04_AnimationBlending) +add_subdirectory(Chapter09/08_ImportLights) +add_subdirectory(Chapter09/09_ImportCameras) + +add_subdirectory(Chapter10/01_OffscreenRendering) +add_subdirectory(Chapter10/02_ShadowMapping) +add_subdirectory(Chapter10/03_MSAA) +add_subdirectory(Chapter10/04_SSAO) +add_subdirectory(Chapter10/05_HDR) +add_subdirectory(Chapter10/06_HDR_Adaptation) diff --git a/Chapter08/VKMesh08.h b/Chapter08/VKMesh08.h index be3ab2b..231f521 100644 --- a/Chapter08/VKMesh08.h +++ b/Chapter08/VKMesh08.h @@ -420,13 +420,14 @@ class VKMesh final frag_ = frag.valid() ? std::move(frag) : loadShaderModule(ctx, "Chapter08/02_SceneGraph/src/main.frag"); pipeline_ = ctx->createRenderPipeline({ - .vertexInput = meshData.streams, - .smVert = vert_, - .smFrag = frag_, - .color = { { .format = colorFormat } }, - .depthFormat = depthFormat, - .cullMode = lvk::CullMode_None, - .samplesCount = numSamples, + .vertexInput = meshData.streams, + .smVert = vert_, + .smFrag = frag_, + .color = { { .format = colorFormat } }, + .depthFormat = depthFormat, + .cullMode = lvk::CullMode_None, + .samplesCount = numSamples, + .minSampleShading = numSamples > 1 ? 0.25f : 0.0f, }); pipelineWireframe_ = ctx->createRenderPipeline({ diff --git a/Chapter10/01_OffscreenRendering/CMakeLists.txt b/Chapter10/01_OffscreenRendering/CMakeLists.txt new file mode 100644 index 0000000..83287ae --- /dev/null +++ b/Chapter10/01_OffscreenRendering/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.16) + +project(Chapter10) + +include(../../CMake/CommonMacros.txt) + +SETUP_APP(Ch10_Sample01_OffscreenRendering "Chapter 10") + +target_link_libraries(Ch10_Sample01_OffscreenRendering PRIVATE SharedUtils) diff --git a/Chapter10/01_OffscreenRendering/src/main.cpp b/Chapter10/01_OffscreenRendering/src/main.cpp new file mode 100644 index 0000000..421cf92 --- /dev/null +++ b/Chapter10/01_OffscreenRendering/src/main.cpp @@ -0,0 +1,190 @@ +#include "shared/VulkanApp.h" + +#include "shared/Utils.h" + +#include <math.h> + +struct VertexData { + float pos[3]; +}; + +const float t = (1.0f + sqrtf(5.0f)) / 2.0f; + +const VertexData vertices[] = { + {-1, t, 0}, + { 1, t, 0}, + {-1, -t, 0}, + { 1, -t, 0}, + + { 0, -1, t}, + { 0, 1, t}, + { 0, -1, -t}, + { 0, 1, -t}, + + { t, 0, -1}, + { t, 0, 1}, + {-t, 0, -1}, + {-t, 0, 1}, +}; + +const uint16_t indices[] = { 0, 11, 5, 0, 5, 1, 0, 1, 7, 0, 7, 10, 0, 10, 11, 1, 5, 9, 5, 11, 4, 11, 10, 2, 10, 7, 6, 7, 1, 8, + 3, 9, 4, 3, 4, 2, 3, 2, 6, 3, 6, 8, 3, 8, 9, 4, 9, 5, 2, 4, 11, 6, 2, 10, 8, 6, 7, 9, 8, 1 }; + +int main() +{ + VulkanApp app({ + .initialCameraPos = vec3(0.0f, 3.0f, -4.5f), + .initialCameraTarget = vec3(0.0f, t, 0.0f), + }); + + std::unique_ptr<lvk::IContext> ctx(app.ctx_.get()); + + // 0. Vertices/indices + lvk::Holder<lvk::BufferHandle> bufferIndices = ctx->createBuffer({ + .usage = lvk::BufferUsageBits_Index, + .storage = lvk::StorageType_Device, + .size = sizeof(indices), + .data = indices, + .debugName = "Buffer: indices", + }); + lvk::Holder<lvk::BufferHandle> bufferVertices = ctx->createBuffer({ + .usage = lvk::BufferUsageBits_Vertex, + .storage = lvk::StorageType_Device, + .size = sizeof(vertices), + .data = vertices, + .debugName = "Buffer: vertices", + }); + + // 1. Shaders & pipeline + lvk::Holder<lvk::ShaderModuleHandle> vert = loadShaderModule(ctx, "Chapter10/01_OffscreenRendering/src/main.vert"); + lvk::Holder<lvk::ShaderModuleHandle> frag = loadShaderModule(ctx, "Chapter10/01_OffscreenRendering/src/main.frag"); + + const lvk::VertexInput vdesc = { + .attributes = { { .location = 0, .format = lvk::VertexFormat::Float3 } }, + .inputBindings = { { .stride = sizeof(VertexData) } }, + }; + + lvk::Holder<lvk::RenderPipelineHandle> pipeline = ctx->createRenderPipeline({ + .vertexInput = vdesc, + .smVert = vert, + .smFrag = frag, + .color = { { .format = ctx->getSwapchainFormat() } }, + .depthFormat = app.getDepthFormat(), + }); + + // 2. Textures and texture views + constexpr uint8_t numMipLevels = lvk::calcNumMipLevels(512, 512); + + lvk::Holder<lvk::TextureHandle> texture = ctx->createTexture({ + .type = lvk::TextureType_2D, + .format = lvk::Format_RGBA_UN8, + .dimensions = {512, 512}, + .usage = lvk::TextureUsageBits_Attachment | lvk::TextureUsageBits_Sampled, + .numMipLevels = numMipLevels, + .debugName = "Texture", + }); + + lvk::Holder<lvk::TextureHandle> mipViews[numMipLevels]; + + for (uint32_t l = 0; l != numMipLevels; l++) { + mipViews[l] = ctx->createTextureView(texture, { .mipLevel = l }); + } + + const vec3 colors[10] = { + {1, 0, 0}, + {0, 1, 0}, + {0, 0, 1}, + + {1, 1, 0}, + {0, 1, 1}, + {1, 0, 1}, + + {1, 0, 0}, + {0, 1, 0}, + {0, 0, 1}, + + {0, 0, 0}, + }; + LVK_ASSERT(LVK_ARRAY_NUM_ELEMENTS(colors) == numMipLevels); + // generate custom mip-pyramid + lvk::ICommandBuffer& buf = ctx->acquireCommandBuffer(); + for (uint8_t i = 0; i != numMipLevels; i++) { + buf.cmdBeginRendering(lvk::RenderPass { + .color = { + {.loadOp = lvk::LoadOp_Clear, .level = i, .clearColor = {colors[i].r,colors[i].g, colors[i].b,1 }}, + } + }, + lvk::Framebuffer{ .color = { { .texture = texture }} }); + buf.cmdEndRendering(); + } + ctx->submit(buf); + + float modelAngle = 0; + bool rotateModel = true; + + app.run([&](uint32_t width, uint32_t height, float aspectRatio, float deltaSeconds) { + if (rotateModel) + modelAngle = fmodf(modelAngle - deltaSeconds, 2.0f * M_PI); + + const mat4 view = app.camera_.getViewMatrix(); + const mat4 proj = glm::perspective(glm::radians(60.0f), aspectRatio, 0.1f, 1000.0f); + const mat4 model = glm::rotate(glm::translate(mat4(1.0f), vec3(0, t, 0)), modelAngle, vec3(1.0f, 1.0f, 1.0f)); + + const lvk::Framebuffer framebuffer = { + .color = { { .texture = ctx->getCurrentSwapchainTexture() } }, + .depthStencil = { .texture = app.getDepthTexture() }, + }; + + lvk::ICommandBuffer& buf = ctx->acquireCommandBuffer(); + buf.cmdBindVertexBuffer(0, bufferVertices); + buf.cmdBindIndexBuffer(bufferIndices, lvk::IndexFormat_UI16); + // 2. Render scene + buf.cmdBeginRendering( + lvk::RenderPass{ + .color = { { .loadOp = lvk::LoadOp_Clear, .clearColor = { 1.0f, 1.0f, 1.0f, 1.0f } } }, + .depth = { .loadOp = lvk::LoadOp_Clear, .clearDepth = 1.0f } + }, + framebuffer); + buf.cmdBindRenderPipeline(pipeline); + buf.cmdBindDepthState({ .compareOp = lvk::CompareOp_Less, .isDepthWriteEnabled = true }); + { + buf.cmdPushDebugGroupLabel("Mesh", 0xff0000ff); + const struct PushConstants { + mat4 mvp; + uint32_t texture; + } pc = { + .mvp = proj * view * model, + .texture = texture.index(), + }; + buf.cmdPushConstants(pc); + buf.cmdDrawIndexed(LVK_ARRAY_NUM_ELEMENTS(indices)); + buf.cmdPopDebugGroupLabel(); + } + app.drawGrid(buf, proj, vec3(0, -0.01f, 0)); + app.imgui_->beginFrame(framebuffer); + app.drawFPS(); + app.drawMemo(); + + const ImGuiViewport* v = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(ImVec2(10, 200)); + ImGui::Begin("Control", nullptr, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::Checkbox("Rotate model", &rotateModel); + ImGui::Separator(); + ImGui::Text("Mip-pyramid 512x512"); + const float windowWidth = v->WorkSize.x / 5; + for (uint32_t l = 0; l != LVK_ARRAY_NUM_ELEMENTS(mipViews); l++) { + ImGui::Image(mipViews[l].index(), ImVec2((int)windowWidth >> l, ((int)windowWidth >> l))); + } + ImGui::Separator(); + ImGui::End(); + + app.imgui_->endFrame(buf); + + buf.cmdEndRendering(); + ctx->submit(buf, ctx->getCurrentSwapchainTexture()); + }); + + ctx.release(); + + return 0; +} diff --git a/Chapter10/01_OffscreenRendering/src/main.frag b/Chapter10/01_OffscreenRendering/src/main.frag new file mode 100644 index 0000000..f7351f9 --- /dev/null +++ b/Chapter10/01_OffscreenRendering/src/main.frag @@ -0,0 +1,14 @@ +// + +layout (location=0) in vec2 uv; + +layout (location=0) out vec4 out_FragColor; + +layout(push_constant) uniform PushConstants { + mat4 mvp; + uint texture; +}; + +void main() { + out_FragColor = textureBindless2D(texture, 0, uv); +} diff --git a/Chapter10/01_OffscreenRendering/src/main.vert b/Chapter10/01_OffscreenRendering/src/main.vert new file mode 100644 index 0000000..b9fe7ba --- /dev/null +++ b/Chapter10/01_OffscreenRendering/src/main.vert @@ -0,0 +1,22 @@ +// + +layout (location = 0) in vec3 pos; +layout (location = 0) out vec2 uv; + +layout(push_constant) uniform PushConstants { + mat4 mvp; +}; + +#define PI 3.1415926 + +float atan2(float y, float x) { + return x == 0.0 ? sign(y) * PI/2 : atan(y, x); +} + +void main() { + gl_Position = mvp * vec4(pos, 1.0); + + float theta = atan2(pos.y, pos.x) / PI + 0.5; + + uv = vec2(theta, pos.z); +} diff --git a/Chapter10/02_ShadowMapping/CMakeLists.txt b/Chapter10/02_ShadowMapping/CMakeLists.txt new file mode 100644 index 0000000..8222ca1 --- /dev/null +++ b/Chapter10/02_ShadowMapping/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.16) + +project(Chapter10) + +include(../../CMake/CommonMacros.txt) + +SETUP_APP(Ch10_Sample02_ShadowMapping "Chapter 10") + +target_link_libraries(Ch10_Sample02_ShadowMapping PRIVATE SharedUtils assimp bc7enc meshoptimizer) diff --git a/Chapter10/02_ShadowMapping/src/common.sp b/Chapter10/02_ShadowMapping/src/common.sp new file mode 100644 index 0000000..d0795f6 --- /dev/null +++ b/Chapter10/02_ShadowMapping/src/common.sp @@ -0,0 +1,25 @@ +// + +layout(std430, buffer_reference) readonly buffer PerFrameData { + mat4 view; + mat4 proj; + mat4 light; + vec4 lightAngles; + vec4 lightPos; + uint shadowTexture; + uint shadowSampler; + float depthBias; +}; + +layout(push_constant) uniform PushConstants { + mat4 model; + PerFrameData perFrame; + uint texture; +} pc; + +struct PerVertex { + vec2 uv; + vec3 worldNormal; + vec3 worldPos; + vec4 shadowCoords; +}; diff --git a/Chapter10/02_ShadowMapping/src/main.cpp b/Chapter10/02_ShadowMapping/src/main.cpp new file mode 100644 index 0000000..18e9b94 --- /dev/null +++ b/Chapter10/02_ShadowMapping/src/main.cpp @@ -0,0 +1,346 @@ +#include "shared/VulkanApp.h" + +#include <assimp/cimport.h> +#include <assimp/postprocess.h> +#include <assimp/scene.h> + +#include "shared/LineCanvas.h" +#include "shared/Utils.h" + +#include "shared/Scene/Scene.h" +#include "shared/Scene/VtxData.h" + +#include "Chapter08/VKMesh08.h" + +#include <math.h> + +int main() +{ + VulkanApp app({ + .initialCameraPos = vec3(0.0f, 3.0f, -4.5f), + .initialCameraTarget = vec3(0.0f, 0.5f, 0.0f), + }); + + LineCanvas3D canvas3d; + + std::unique_ptr<lvk::IContext> ctx(app.ctx_.get()); + + struct VertexData { + vec3 pos; + vec3 n; + vec2 tc; + }; + + // 0. Scene vertices/indices + std::vector<VertexData> vertices; + std::vector<uint32_t> indices; + + // 1. Duck + { + const aiScene* scene = aiImportFile("data/rubber_duck/scene.gltf", aiProcess_Triangulate); + + if (!scene || !scene->HasMeshes()) { + printf("Unable to load data/rubber_duck/scene.gltf\n"); + exit(255); + } + + const aiMesh* mesh = scene->mMeshes[0]; + for (uint32_t i = 0; i != mesh->mNumVertices; i++) { + const aiVector3D v = mesh->mVertices[i]; + const aiVector3D n = mesh->mNormals[i]; + const aiVector3D t = mesh->mTextureCoords[0][i]; + vertices.push_back({ .pos = vec3(v.x, v.y, v.z), .n = vec3(n.x, n.y, n.z), .tc = vec2(t.x, t.y) }); + } + for (uint32_t i = 0; i != mesh->mNumFaces; i++) { + for (uint32_t j = 0; j != 3; j++) + indices.push_back(mesh->mFaces[i].mIndices[j]); + } + aiReleaseImport(scene); + } + + const uint32_t duckNumIndices = (uint32_t)indices.size(); + const uint32_t planeVertexOffset = (uint32_t)vertices.size(); + + // 2. Plane + mergeVectors(indices, { 0, 1, 2, 2, 3, 0 }); + mergeVectors( + vertices, { + {vec3(-4, -4, 0), vec3(0, 0, 1), vec2(0, 0)}, + {vec3(-4, +4, 0), vec3(0, 0, 1), vec2(0, 1)}, + {vec3(+4, +4, 0), vec3(0, 0, 1), vec2(1, 1)}, + {vec3(+4, -4, 0), vec3(0, 0, 1), vec2(1, 0)}, + }); + + lvk::Holder<lvk::BufferHandle> bufferIndices = ctx->createBuffer({ + .usage = lvk::BufferUsageBits_Index, + .storage = lvk::StorageType_Device, + .size = sizeof(uint32_t) * indices.size(), + .data = indices.data(), + .debugName = "Buffer: indices", + }); + + lvk::Holder<lvk::BufferHandle> bufferVertices = ctx->createBuffer({ + .usage = lvk::BufferUsageBits_Vertex, + .storage = lvk::StorageType_Device, + .size = sizeof(VertexData) * vertices.size(), + .data = vertices.data(), + .debugName = "Buffer: vertices", + }); + + // Textures + lvk::Holder<lvk::TextureHandle> duckTexture = loadTexture(ctx, "data/rubber_duck/textures/Duck_baseColor.png"); + lvk::Holder<lvk::TextureHandle> planeTexture = loadTexture(ctx, "data/wood.jpg"); + + struct PerFrameData { + mat4 view; + mat4 proj; + mat4 light; + vec4 lightAngles; // cos(inner), cos(outer) + vec4 lightPos; + uint32_t shadowTexture; + uint32_t shadowSampler; + float depthBias; + }; + lvk::Holder<lvk::BufferHandle> bufferPerFrame = ctx->createBuffer({ + .usage = lvk::BufferUsageBits_Uniform, + .storage = lvk::StorageType_Device, + .size = sizeof(PerFrameData), + .debugName = "Buffer: per-frame", + }); + + lvk::Holder<lvk::TextureHandle> shadowMap = ctx->createTexture({ + .type = lvk::TextureType_2D, + .format = lvk::Format_Z_UN16, + .dimensions = {1024, 1024}, + .usage = lvk::TextureUsageBits_Attachment | lvk::TextureUsageBits_Sampled, + .debugName = "Shadow map", + }); + + lvk::Holder<lvk::SamplerHandle> samplerShadow = ctx->createSampler({ + .wrapU = lvk::SamplerWrap_Clamp, + .wrapV = lvk::SamplerWrap_Clamp, + .depthCompareOp = lvk::CompareOp_LessEqual, + .depthCompareEnabled = true, + .debugName = "Sampler: shadow", + }); + + lvk::Holder<lvk::ShaderModuleHandle> vert = loadShaderModule(ctx, "Chapter10/02_ShadowMapping/src/main.vert"); + lvk::Holder<lvk::ShaderModuleHandle> frag = loadShaderModule(ctx, "Chapter10/02_ShadowMapping/src/main.frag"); + lvk::Holder<lvk::ShaderModuleHandle> vertShadow = loadShaderModule(ctx, "Chapter10/02_ShadowMapping/src/shadow.vert"); + lvk::Holder<lvk::ShaderModuleHandle> fragShadow = loadShaderModule(ctx, "Chapter10/02_ShadowMapping/src/shadow.frag"); + + const lvk::VertexInput vdesc = { + .attributes = {{ .location = 0, .format = lvk::VertexFormat::Float3, .offset = offsetof(VertexData, pos) }, + { .location = 1, .format = lvk::VertexFormat::Float3, .offset = offsetof(VertexData, n) }, + { .location = 2, .format = lvk::VertexFormat::Float2, .offset = offsetof(VertexData, tc) }, }, + .inputBindings = { { .stride = sizeof(VertexData) } }, + }; + + lvk::Holder<lvk::RenderPipelineHandle> pipeline = ctx->createRenderPipeline({ + .vertexInput = vdesc, + .smVert = vert, + .smFrag = frag, + .color = { { .format = ctx->getSwapchainFormat() } }, + .depthFormat = app.getDepthFormat(), + }); + + lvk::Holder<lvk::RenderPipelineHandle> pipelineShadow = ctx->createRenderPipeline({ + .vertexInput = + lvk::VertexInput{ + .attributes = { { .location = 0, .format = lvk::VertexFormat::Float3, .offset = offsetof(VertexData, pos) } }, + .inputBindings = { { .stride = sizeof(VertexData) } }, + }, + .smVert = vertShadow, + .smFrag = fragShadow, + .depthFormat = ctx->getFormat(shadowMap), + }); + + float g_LightFOV = 45.0f; + float g_LightInnerAngle = 10.0f; + float g_LightNear = 0.8f; + float g_LightFar = 8.0f; + + float g_LightDist = 4.0f; + float g_LightXAngle = 240.0f; + float g_LightYAngle = 0.0f; + + float g_LightDepthBias = -0.005f; + + bool g_RotateModel = true; + bool g_RotateLight = true; + bool g_DrawFrustum = true; + + float g_ModelAngle = 0; + float g_LightAngle = 0; + + const char* comboBoxItems[] = { "First person", "Light source" }; + const char* cameraType = comboBoxItems[0]; + const char* currentComboBoxItem = cameraType; + + app.run([&](uint32_t width, uint32_t height, float aspectRatio, float deltaSeconds) { + if (g_RotateModel) + g_ModelAngle = fmodf(g_ModelAngle - 50.0f * deltaSeconds, 360.0f); + if (g_RotateLight) + g_LightYAngle = fmodf(g_LightYAngle + 50.0f * deltaSeconds, 360.0f); + + // 0. Calculate light and camera parameters + const mat4 rotY = glm::rotate(mat4(1.f), glm::radians(g_LightYAngle), vec3(0, 1, 0)); + const mat4 rotX = glm::rotate(rotY, glm::radians(g_LightXAngle), vec3(1, 0, 0)); + const vec4 lightPos = rotX * vec4(0, 0, g_LightDist, 1.0f); + + const mat4 lightProj = glm::perspective(glm::radians(g_LightFOV), 1.0f, g_LightNear, g_LightFar); + const mat4 lightView = glm::lookAt(vec3(lightPos), vec3(0), vec3(0, 1, 0)); + + const bool showLightCamera = cameraType == comboBoxItems[1]; + + const mat4 view = showLightCamera ? lightView : app.camera_.getViewMatrix(); + const mat4 proj = showLightCamera ? glm::perspective(glm::radians(g_LightFOV), aspectRatio, g_LightNear, g_LightFar) + : glm::perspective(glm::radians(60.0f), aspectRatio, 0.1f, 1000.0f); + const mat4 m1 = glm::rotate(mat4(1.0f), glm::radians(-90.0f), vec3(1, 0, 0)); + const mat4 m2 = glm::rotate(mat4(1.0f), glm::radians(g_ModelAngle), vec3(0.0f, 1.0f, 0.0f)); + + const lvk::Framebuffer framebuffer = { + .color = { { .texture = ctx->getCurrentSwapchainTexture() } }, + .depthStencil = { .texture = app.getDepthTexture() }, + }; + + lvk::ICommandBuffer& buf = ctx->acquireCommandBuffer(); + buf.cmdBindVertexBuffer(0, bufferVertices); + buf.cmdBindIndexBuffer(bufferIndices, lvk::IndexFormat_UI32); + struct PushConstants { + mat4 model; + uint64_t perFrameBuffer; + uint32_t texture; + }; + // 1. Render shadow map + buf.cmdUpdateBuffer( + bufferPerFrame, PerFrameData{ + .view = lightView, + .proj = lightProj, + }); + buf.cmdBeginRendering( + lvk::RenderPass{ + .depth = {.loadOp = lvk::LoadOp_Clear, .clearDepth = 1.0f} + }, + lvk::Framebuffer{ .depthStencil = { .texture = shadowMap } }); + + buf.cmdBindRenderPipeline(pipelineShadow); + buf.cmdPushConstants(PushConstants{ + .model = m2 * m1, + .perFrameBuffer = ctx->gpuAddress(bufferPerFrame), + }); + buf.cmdBindDepthState({ .compareOp = lvk::CompareOp_Less, .isDepthWriteEnabled = true }); + buf.cmdDrawIndexed(duckNumIndices); + buf.cmdEndRendering(); + // 2. Render scene + const mat4 scaleBias = mat4(0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.5, 0.5, 0.0, 1.0); + buf.cmdUpdateBuffer( + bufferPerFrame, + PerFrameData{ + .view = view, + .proj = proj, + .light = scaleBias * lightProj * lightView, + .lightAngles = + vec4(cosf(glm::radians(0.5f * g_LightFOV)), cosf(glm::radians(0.5f * (g_LightFOV - g_LightInnerAngle))), 1.0f, 1.0f), + .lightPos = lightPos, + .shadowTexture = shadowMap.index(), + .shadowSampler = samplerShadow.index(), + .depthBias = g_LightDepthBias, + }); + + buf.cmdBeginRendering( + lvk::RenderPass{ + .color = { { .loadOp = lvk::LoadOp_Clear, .clearColor = { 1.0f, 1.0f, 1.0f, 1.0f } } }, + .depth = { .loadOp = lvk::LoadOp_Clear, .clearDepth = 1.0f } + }, + framebuffer, { .textures = { { lvk::TextureHandle(shadowMap) } } }); + buf.cmdBindRenderPipeline(pipeline); + buf.cmdBindDepthState({ .compareOp = lvk::CompareOp_Less, .isDepthWriteEnabled = true }); + { + buf.cmdPushDebugGroupLabel("Mesh", 0xff0000ff); + buf.cmdPushConstants(PushConstants{ + .model = m2 * m1, + .perFrameBuffer = ctx->gpuAddress(bufferPerFrame), + .texture = duckTexture.index(), + }); + buf.cmdDrawIndexed(duckNumIndices); + buf.cmdPopDebugGroupLabel(); + } + { + buf.cmdPushDebugGroupLabel("Plane", 0xff0000ff); + buf.cmdPushConstants(PushConstants{ + .model = m1, + .perFrameBuffer = ctx->gpuAddress(bufferPerFrame), + .texture = planeTexture.index(), + }); + buf.cmdDrawIndexed(6, 1, 0, planeVertexOffset); + buf.cmdPopDebugGroupLabel(); + } + if (!showLightCamera) + app.drawGrid(buf, proj, vec3(0, -0.01f, 0)); + app.imgui_->beginFrame(framebuffer); + app.drawFPS(); + app.drawMemo(); + + const ImGuiViewport* v = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(ImVec2(10, 200)); + ImGui::Begin("Control", nullptr, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::Checkbox("Rotate model", &g_RotateModel); + ImGui::Separator(); + ImGui::Text("Light parameters", nullptr); + const float indentSize = 16.0f; + ImGui::Indent(indentSize); + ImGui::Checkbox("Rotate light", &g_RotateLight); + ImGui::Checkbox("Draw light frustum", &g_DrawFrustum); + ImGui::SliderFloat("Depth bias", &g_LightDepthBias, -0.01f, 0.0f); + ImGui::SliderFloat("Proj::Light FOV", &g_LightFOV, 15.0f, 120.0f); + ImGui::SliderFloat("Proj::Light inner angle", &g_LightInnerAngle, 1.0f, 15.0f); + ImGui::SliderFloat("Proj::Near", &g_LightNear, 0.1f, 3.0f); + ImGui::SliderFloat("Proj::Far", &g_LightFar, 1.0f, 20.0f); + ImGui::SliderFloat("Pos::Dist", &g_LightDist, 1.0f, 10.0f); + ImGui::SliderFloat("Pos::AngleX", &g_LightXAngle, 0, 360.0f); + ImGui::BeginDisabled(g_RotateLight); + ImGui::SliderFloat("Pos::AngleY", &g_LightYAngle, 0, 360.0f); + ImGui::EndDisabled(); + ImGui::Unindent(indentSize); + ImGui::Separator(); + ImGui::Image(shadowMap.index(), ImVec2(512, 512)); + ImGui::Separator(); + // camera controls + { + if (ImGui::BeginCombo("Camera", currentComboBoxItem)) // the second parameter is the label previewed before opening the combo. + { + for (int n = 0; n < IM_ARRAYSIZE(comboBoxItems); n++) { + const bool isSelected = (currentComboBoxItem == comboBoxItems[n]); + if (ImGui::Selectable(comboBoxItems[n], isSelected)) + currentComboBoxItem = comboBoxItems[n]; + if (isSelected) + ImGui::SetItemDefaultFocus(); // initial focus when opening the combo (scrolling + for keyboard navigation support) + } + ImGui::EndCombo(); + } + if (currentComboBoxItem && strcmp(currentComboBoxItem, cameraType)) { + printf("Selected new camera type: %s\n", currentComboBoxItem); + cameraType = currentComboBoxItem; + } + } + + ImGui::End(); + + if (!showLightCamera && g_DrawFrustum) { + canvas3d.clear(); + canvas3d.setMatrix(proj * view); + canvas3d.frustum(lightView, lightProj, vec4(1, 0, 0, 1)); + canvas3d.render(*ctx.get(), framebuffer, buf); + } + + app.imgui_->endFrame(buf); + + buf.cmdEndRendering(); + ctx->submit(buf, ctx->getCurrentSwapchainTexture()); + }); + + ctx.release(); + + return 0; +} diff --git a/Chapter10/02_ShadowMapping/src/main.frag b/Chapter10/02_ShadowMapping/src/main.frag new file mode 100644 index 0000000..e7425c5 --- /dev/null +++ b/Chapter10/02_ShadowMapping/src/main.frag @@ -0,0 +1,53 @@ +// + +#include <Chapter10/02_ShadowMapping/src/common.sp> + +layout (location=0) in PerVertex vtx; + +layout (location=0) out vec4 out_FragColor; + +float PCF3(vec3 uvw) { + float size = 1.0 / textureBindlessSize2D(pc.perFrame.shadowTexture).x; + float shadow = 0.0; + for (int v=-1; v<=+1; v++) + for (int u=-1; u<=+1; u++) + shadow += textureBindless2DShadow(pc.perFrame.shadowTexture, pc.perFrame.shadowSampler, uvw + size * vec3(u, v, 0)); + return shadow / 9; +} + +float shadow(vec4 s) { + s = s / s.w; + if (s.z > -1.0 && s.z < 1.0) { + float shadowSample = PCF3(vec3(s.x, 1.0 - s.y, s.z + pc.perFrame.depthBias)); + return mix(0.3, 1.0, shadowSample); + } + return 1.0; +} + +float spotLightFactor(vec3 worldPos) +{ + vec3 dirLight = normalize(worldPos - pc.perFrame.lightPos.xyz); + vec3 dirSpot = normalize(-pc.perFrame.lightPos.xyz); // light is always looking at (0, 0, 0) + + float rho = dot(dirLight, dirSpot); + + float outerAngle = pc.perFrame.lightAngles.x; + float innerAngle = pc.perFrame.lightAngles.y; + + if (rho > outerAngle) + return smoothstep(outerAngle, innerAngle, rho); + + return 0.0; +} + +void main() { + vec3 n = normalize(vtx.worldNormal); + vec3 l = normalize(pc.perFrame.lightPos.xyz); + + float NdotL = clamp(dot(n, l), 0.1, 1.0); + + float Ka = 0.1; + float Kd = NdotL * shadow(vtx.shadowCoords) * spotLightFactor(vtx.worldPos); + + out_FragColor = textureBindless2D(pc.texture, 0, vtx.uv) * clamp(Ka + Kd, 0.3, 1.0); +} diff --git a/Chapter10/02_ShadowMapping/src/main.vert b/Chapter10/02_ShadowMapping/src/main.vert new file mode 100644 index 0000000..b98ea1c --- /dev/null +++ b/Chapter10/02_ShadowMapping/src/main.vert @@ -0,0 +1,20 @@ +// + +#include <Chapter10/02_ShadowMapping/src/common.sp> + +layout (location = 0) in vec3 pos; +layout (location = 1) in vec3 normal; +layout (location = 2) in vec2 uv; + +layout (location=0) out PerVertex vtx; + +void main() { + gl_Position = pc.perFrame.proj * pc.perFrame.view * pc.model * vec4(pos, 1.0); + + mat3 normalMatrix = transpose( inverse(mat3(pc.model)) ); + + vtx.uv = uv; + vtx.worldNormal = normalMatrix * normal; + vtx.worldPos = (pc.model * vec4(pos, 1.0)).xyz; + vtx.shadowCoords = pc.perFrame.light * pc.model * vec4(pos, 1.0); +} diff --git a/Chapter10/02_ShadowMapping/src/shadow.frag b/Chapter10/02_ShadowMapping/src/shadow.frag new file mode 100644 index 0000000..574edb7 --- /dev/null +++ b/Chapter10/02_ShadowMapping/src/shadow.frag @@ -0,0 +1,4 @@ +// + +void main() { +} diff --git a/Chapter10/02_ShadowMapping/src/shadow.vert b/Chapter10/02_ShadowMapping/src/shadow.vert new file mode 100644 index 0000000..80824f9 --- /dev/null +++ b/Chapter10/02_ShadowMapping/src/shadow.vert @@ -0,0 +1,10 @@ +// + +#include <Chapter10/02_ShadowMapping/src/common.sp> + +layout (location = 0) in vec3 pos; + +void main() { + gl_Position = pc.perFrame.proj * pc.perFrame.view * pc.model * vec4(pos, 1.0); +} + diff --git a/Chapter10/03_MSAA/CMakeLists.txt b/Chapter10/03_MSAA/CMakeLists.txt new file mode 100644 index 0000000..e1196ba --- /dev/null +++ b/Chapter10/03_MSAA/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.16) + +project(Chapter10) + +include(../../CMake/CommonMacros.txt) + +SETUP_APP(Ch10_Sample03_MSAA "Chapter 10") + +target_link_libraries(Ch10_Sample03_MSAA PRIVATE SharedUtils assimp bc7enc meshoptimizer) diff --git a/Chapter10/03_MSAA/src/main.cpp b/Chapter10/03_MSAA/src/main.cpp new file mode 100644 index 0000000..a80c8de --- /dev/null +++ b/Chapter10/03_MSAA/src/main.cpp @@ -0,0 +1,122 @@ +#include "shared/VulkanApp.h" + +#include "Chapter10/Bistro.h" + +int main() +{ + MeshData meshData; + Scene scene; + loadBistro(meshData, scene); + + VulkanApp app({ + .initialCameraPos = vec3(-19.261f, 8.465f, -7.317f), + .initialCameraTarget = vec3(0, +2.5f, 0), + }); + + app.positioner_.maxSpeed_ = 1.5f; + + std::unique_ptr<lvk::IContext> ctx(app.ctx_.get()); + + lvk::Holder<lvk::TextureHandle> texSkyboxIrradiance = loadTexture(ctx, "data/immenstadter_horn_2k_irradiance.ktx", lvk::TextureType_Cube); + + const uint32_t kNumSamples = 8; + + const lvk::Dimensions sizeFb = ctx->getDimensions(ctx->getCurrentSwapchainTexture()); + + lvk::Holder<lvk::TextureHandle> msaaColor = ctx->createTexture({ + .format = ctx->getSwapchainFormat(), + .dimensions = sizeFb, + .numSamples = kNumSamples, + .usage = lvk::TextureUsageBits_Attachment, + .debugName = "msaaColor", + }); + lvk::Holder<lvk::TextureHandle> msaaDepth = ctx->createTexture({ + .format = app.getDepthFormat(), + .dimensions = sizeFb, + .numSamples = kNumSamples, + .usage = lvk::TextureUsageBits_Attachment, + .debugName = "msaaDepth", + }); + + bool enableMSAA = true; + bool drawWireframe = false; + bool drawBoundingBoxes = false; + + const VKMesh mesh(ctx, meshData, scene, ctx->getSwapchainFormat(), app.getDepthFormat()); + const VKMesh meshMSAA(ctx, meshData, scene, ctx->getSwapchainFormat(), app.getDepthFormat(), kNumSamples); + + LineCanvas3D canvas3d; + + app.run([&](uint32_t width, uint32_t height, float aspectRatio, float deltaSeconds) { + const mat4 view = app.camera_.getViewMatrix(); + const mat4 proj = glm::perspective(45.0f, aspectRatio, 0.01f, 1000.0f); + + lvk::ICommandBuffer& buf = ctx->acquireCommandBuffer(); + { + const lvk::Framebuffer framebufferOffscreen = { + .color = { { .texture = enableMSAA ? msaaColor : ctx->getCurrentSwapchainTexture(), + .resolveTexture = enableMSAA ? ctx->getCurrentSwapchainTexture() : lvk::TextureHandle{} } }, + .depthStencil = { .texture = enableMSAA ? msaaDepth : app.getDepthTexture() }, + }; + // 1. Render scene + buf.cmdBeginRendering( + lvk::RenderPass{ + .color = { { .loadOp = lvk::LoadOp_Clear, + .storeOp = enableMSAA ? lvk::StoreOp_MsaaResolve : lvk::StoreOp_Store, + .clearColor = { 1.0f, 1.0f, 1.0f, 1.0f } } }, + .depth = { .loadOp = lvk::LoadOp_Clear, .clearDepth = 1.0f } + }, + framebufferOffscreen); + buf.cmdPushDebugGroupLabel("Mesh", 0xff0000ff); + (enableMSAA ? meshMSAA : mesh).draw(*ctx.get(), buf, view, proj, texSkyboxIrradiance, drawWireframe); + buf.cmdPopDebugGroupLabel(); + app.drawGrid(buf, proj, vec3(0, -1.0f, 0), enableMSAA ? kNumSamples : 1); + canvas3d.render(*ctx.get(), framebufferOffscreen, buf, enableMSAA ? kNumSamples : 1); + buf.cmdEndRendering(); + + // 2. Render UI + const lvk::Framebuffer framebufferMain = { + .color = { { .texture = ctx->getCurrentSwapchainTexture() } }, + }; + buf.cmdBeginRendering( + lvk::RenderPass{ + .color = { { .loadOp = lvk::LoadOp_Load, .clearColor = { 1.0f, 1.0f, 1.0f, 1.0f } } }, + }, + framebufferMain); + + app.imgui_->beginFrame(framebufferMain); + app.drawFPS(); + app.drawMemo(); + + canvas3d.clear(); + canvas3d.setMatrix(proj * view); + // render all bounding boxes (red) + if (drawBoundingBoxes) { + for (auto& p : scene.meshForNode) { + const BoundingBox box = meshData.boxes[p.second]; + canvas3d.box(scene.globalTransform[p.first], box, vec4(1, 0, 0, 1)); + } + } + + { + const ImGuiViewport* v = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(ImVec2(10, 200)); + ImGui::Begin( + "MSAA", nullptr, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize); + ImGui::Checkbox("Enable MSAA", &enableMSAA); + ImGui::Checkbox("Draw wireframe", &drawWireframe); + ImGui::Checkbox("Draw bounding boxes", &drawBoundingBoxes); + ImGui::End(); + } + + app.imgui_->endFrame(buf); + + buf.cmdEndRendering(); + } + ctx->submit(buf, ctx->getCurrentSwapchainTexture()); + }); + + ctx.release(); + + return 0; +} diff --git a/Chapter10/04_SSAO/CMakeLists.txt b/Chapter10/04_SSAO/CMakeLists.txt new file mode 100644 index 0000000..547048e --- /dev/null +++ b/Chapter10/04_SSAO/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.16) + +project(Chapter10) + +include(../../CMake/CommonMacros.txt) + +SETUP_APP(Ch10_Sample04_SSAO "Chapter 10") + +target_link_libraries(Ch10_Sample04_SSAO PRIVATE SharedUtils assimp bc7enc meshoptimizer) diff --git a/Chapter10/04_SSAO/src/SSAO.comp b/Chapter10/04_SSAO/src/SSAO.comp new file mode 100644 index 0000000..793959d --- /dev/null +++ b/Chapter10/04_SSAO/src/SSAO.comp @@ -0,0 +1,70 @@ +// +layout (local_size_x = 16, local_size_y = 16) in; + +layout (set = 0, binding = 0) uniform texture2D kTextures2D[]; +layout (set = 0, binding = 1) uniform sampler kSamplers[]; + +layout (set = 0, binding = 2, rgba8) uniform writeonly image2D kTextures2DOut[]; + +layout(push_constant) uniform PushConstants { + uint texDepth; + uint texRotation; + uint texOut; + uint smpl; + float zNear; + float zFar; + float radius; + float attScale; + float distScale; +} pc; + +ivec2 textureBindlessSize2D(uint textureid) { + return textureSize(nonuniformEXT(kTextures2D[textureid]), 0); +} + +vec4 textureBindless2D(uint textureid, vec2 uv) { + return textureLod(nonuniformEXT(sampler2D(kTextures2D[textureid], kSamplers[pc.smpl])), uv, 0); +} + +const vec3 offsets[8] = vec3[8]( + vec3(-0.5, -0.5, -0.5), + vec3( 0.5, -0.5, -0.5), + vec3(-0.5, 0.5, -0.5), + vec3( 0.5, 0.5, -0.5), + vec3(-0.5, -0.5, 0.5), + vec3( 0.5, -0.5, 0.5), + vec3(-0.5, 0.5, 0.5), + vec3( 0.5, 0.5, 0.5) +); + +float scaleZ(float smpl) { + return (pc.zFar * pc.zNear) / (smpl * (pc.zFar-pc.zNear) - pc.zFar); +} + +void main() { + const vec2 size = textureBindlessSize2D(pc.texDepth).xy; + + const vec2 xy = gl_GlobalInvocationID.xy; + const vec2 uv = (gl_GlobalInvocationID.xy + vec2(0.5)) / size; + + if (xy.x > size.x || xy.y > size.y) + return; + + const float Z = scaleZ( textureBindless2D(pc.texDepth, uv).x ); + const vec3 plane = textureBindless2D(pc.texRotation, xy / 4.0).xyz - vec3(1.0); + + float att = 0.0; + + for ( int i = 0; i < 8; i++ ) + { + vec3 rSample = reflect( offsets[i], plane ); + float zSample = scaleZ( textureBindless2D( pc.texDepth, uv + pc.radius*rSample.xy / Z ).x ); + float dist = max(zSample - Z, 0.0) / pc.distScale; + float occl = 15.0 * max( dist * (2.0 - dist), 0.0 ); + att += 1.0 / (1.0 + occl*occl); + } + + att = clamp(att * att / 64.0 + 0.45, 0.0, 1.0) * pc.attScale; + + imageStore(kTextures2DOut[pc.texOut], ivec2(xy), vec4( vec3(att), 1.0 ) ); +} diff --git a/Chapter10/04_SSAO/src/combine.frag b/Chapter10/04_SSAO/src/combine.frag new file mode 100644 index 0000000..45607f9 --- /dev/null +++ b/Chapter10/04_SSAO/src/combine.frag @@ -0,0 +1,23 @@ +// + +layout (location=0) in vec2 uv; + +layout (location=0) out vec4 out_FragColor; + +layout(push_constant) uniform PushConstants { + uint texColor; + uint texSSAO; + uint smpl; + float scale; + float bias; +} pc; + +void main() { + vec4 color = textureBindless2D(pc.texColor, pc.smpl, uv); + float ssao = clamp( textureBindless2D(pc.texSSAO, pc.smpl, uv).x + pc.bias, 0.0, 1.0 ); + + out_FragColor = vec4( + mix(color, color * ssao, pc.scale).rgb, + 1.0 + ); +} diff --git a/Chapter10/04_SSAO/src/combine.vert b/Chapter10/04_SSAO/src/combine.vert new file mode 100644 index 0000000..4d00cbb --- /dev/null +++ b/Chapter10/04_SSAO/src/combine.vert @@ -0,0 +1,9 @@ +// + +layout (location=0) out vec2 uv; + +void main() { + // generate a triangle covering the entire screen + uv = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); + gl_Position = vec4(uv * vec2(2, -2) + vec2(-1, 1), 0.0, 1.0); +} diff --git a/Chapter10/04_SSAO/src/main.cpp b/Chapter10/04_SSAO/src/main.cpp new file mode 100644 index 0000000..584cc8d --- /dev/null +++ b/Chapter10/04_SSAO/src/main.cpp @@ -0,0 +1,317 @@ +#include "shared/VulkanApp.h" + +#include "Chapter10/Bistro.h" +#include "Chapter10/Skybox.h" + +int main() +{ + MeshData meshData; + Scene scene; + loadBistro(meshData, scene); + + VulkanApp app({ + .initialCameraPos = vec3(-19.261f, 8.465f, -7.317f), + .initialCameraTarget = vec3(0, +2.5f, 0), + }); + + app.positioner_.maxSpeed_ = 1.5f; + + std::unique_ptr<lvk::IContext> ctx(app.ctx_.get()); + + lvk::Holder<lvk::ShaderModuleHandle> compSSAO = loadShaderModule(ctx, "Chapter10/04_SSAO/src/SSAO.comp"); + lvk::Holder<lvk::ComputePipelineHandle> pipelineSSAO = ctx->createComputePipeline({ + .smComp = compSSAO, + }); + + const uint32_t kHorizontal = 1; + const uint32_t kVertical = 0; + + lvk::Holder<lvk::ShaderModuleHandle> compBlur = loadShaderModule(ctx, "data/shaders/Blur.comp"); + lvk::Holder<lvk::ComputePipelineHandle> pipelineBlurX = ctx->createComputePipeline({ + .smComp = compBlur, + .specInfo = {.entries = { { .constantId = 0, .size = sizeof(uint32_t) } }, .data = &kHorizontal, .dataSize = sizeof(uint32_t)}, + }); + lvk::Holder<lvk::ComputePipelineHandle> pipelineBlurY = ctx->createComputePipeline({ + .smComp = compBlur, + .specInfo = {.entries = { { .constantId = 0, .size = sizeof(uint32_t) } }, .data = &kVertical, .dataSize = sizeof(uint32_t)}, + }); + + lvk::Holder<lvk::ShaderModuleHandle> vertCombine = loadShaderModule(ctx, "data/shaders/QuadFlip.vert"); + lvk::Holder<lvk::ShaderModuleHandle> fragCombine = loadShaderModule(ctx, "Chapter10/04_SSAO/src/combine.frag"); + lvk::Holder<lvk::RenderPipelineHandle> pipelineCombine = ctx->createRenderPipeline({ + .smVert = vertCombine, + .smFrag = fragCombine, + .color = { { .format = ctx->getSwapchainFormat() } }, + }); + + lvk::Holder<lvk::TextureHandle> texSSAO = ctx->createTexture({ + .format = ctx->getSwapchainFormat(), + .dimensions = ctx->getDimensions(ctx->getCurrentSwapchainTexture()), + .usage = lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .debugName = "texSSAO", + }); + lvk::Holder<lvk::TextureHandle> texBlur[] = { + ctx->createTexture({ + .format = ctx->getSwapchainFormat(), + .dimensions = ctx->getDimensions(ctx->getCurrentSwapchainTexture()), + .usage = lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .debugName = "texBlur0", + }), + ctx->createTexture({ + .format = ctx->getSwapchainFormat(), + .dimensions = ctx->getDimensions(ctx->getCurrentSwapchainTexture()), + .usage = lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .debugName = "texBlur1", + }), + }; + + const lvk::Dimensions sizeFb = ctx->getDimensions(ctx->getCurrentSwapchainTexture()); + const lvk::Dimensions sizeOffscreen = { sizeFb.width, sizeFb.height }; + + const uint32_t kNumSamples = 8; + + lvk::Holder<lvk::TextureHandle> msaaColor = ctx->createTexture({ + .format = ctx->getSwapchainFormat(), + .dimensions = sizeFb, + .numSamples = kNumSamples, + .usage = lvk::TextureUsageBits_Attachment, + .debugName = "msaaColor", + }); + lvk::Holder<lvk::TextureHandle> msaaDepth = ctx->createTexture({ + .format = app.getDepthFormat(), + .dimensions = sizeFb, + .numSamples = kNumSamples, + .usage = lvk::TextureUsageBits_Attachment, + .debugName = "msaaDepth", + }); + + lvk::Holder<lvk::TextureHandle> offscreenColor = ctx->createTexture({ + .format = ctx->getSwapchainFormat(), + .dimensions = sizeOffscreen, + .usage = lvk::TextureUsageBits_Attachment | lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .debugName = "offscreenColor", + }); + lvk::Holder<lvk::TextureHandle> offscreenDepth = ctx->createTexture({ + .format = app.getDepthFormat(), + .dimensions = sizeOffscreen, + .usage = lvk::TextureUsageBits_Attachment | lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .debugName = "offscreenDepth", + }); + + lvk::Holder<lvk::TextureHandle> texRotations = loadTexture(ctx, "data/rot_texture.bmp"); + + lvk::Holder<lvk::SamplerHandle> samplerClamp = ctx->createSampler({ + .wrapU = lvk::SamplerWrap_Clamp, + .wrapV = lvk::SamplerWrap_Clamp, + .wrapW = lvk::SamplerWrap_Clamp, + }); + + bool drawWireframe = false; + bool enableBlur = true; + int numBlurPasses = 1; + + enum DrawMode { + DrawMode_ColorSSAO = 0, + DrawMode_Color = 1, + DrawMode_SSAO = 2, + }; + + int drawMode = DrawMode_ColorSSAO; + float depthThreshold = 30.0f; // bilateral blur + + struct { + uint32_t texDepth; + uint32_t texRotation; + uint32_t texOut; + uint32_t sampler; + float zNear; + float zFar; + float radius; + float attScale; + float distScale; + } pcSSAO = { + .texDepth = offscreenDepth.index(), + .texRotation = texRotations.index(), + .texOut = texSSAO.index(), + .sampler = samplerClamp.index(), + .zNear = 0.01f, + .zFar = 1000.0f, + .radius = 0.03f, + .attScale = 0.95f, + .distScale = 1.7f, + }; + + struct { + uint32_t texColor; + uint32_t texSSAO; + uint32_t sampler; + float scale; + float bias; + } pcCombine = { + .texColor = offscreenColor.index(), + .texSSAO = texSSAO.index(), + .sampler = samplerClamp.index(), + .scale = 1.5f, + .bias = 0.16f, + }; + + const Skybox skyBox( + ctx, "data/immenstadter_horn_2k_prefilter.ktx", "data/immenstadter_horn_2k_irradiance.ktx", ctx->getSwapchainFormat(), + app.getDepthFormat(), kNumSamples); + const VKMesh mesh(ctx, meshData, scene, ctx->getSwapchainFormat(), app.getDepthFormat(), kNumSamples); + + app.run([&](uint32_t width, uint32_t height, float aspectRatio, float deltaSeconds) { + const mat4 view = app.camera_.getViewMatrix(); + const mat4 proj = glm::perspective(45.0f, aspectRatio, pcSSAO.zNear, pcSSAO.zFar); + + lvk::ICommandBuffer& buf = ctx->acquireCommandBuffer(); + { + // 1. Render scene + buf.cmdBeginRendering( + lvk::RenderPass{ + .color = { { .loadOp = lvk::LoadOp_Clear, .storeOp = lvk::StoreOp_MsaaResolve, .clearColor = { 1.0f, 1.0f, 1.0f, 1.0f } } }, + .depth = { .loadOp = lvk::LoadOp_Clear, .storeOp = lvk::StoreOp_MsaaResolve, .clearDepth = 1.0f } + }, + lvk::Framebuffer{ + .color = { { .texture = msaaColor, .resolveTexture = offscreenColor } }, + .depthStencil = { .texture = msaaDepth, .resolveTexture = offscreenDepth }, + }); + skyBox.draw(buf, view, proj); + { + buf.cmdPushDebugGroupLabel("Mesh", 0xff0000ff); + mesh.draw(*ctx.get(), buf, view, proj, skyBox.texSkyboxIrradiance, drawWireframe); + buf.cmdPopDebugGroupLabel(); + } + app.drawGrid(buf, proj, vec3(0, -1.0f, 0), kNumSamples); + buf.cmdEndRendering(); + + // 2. Compute SSAO + buf.cmdBindComputePipeline(pipelineSSAO); + buf.cmdPushConstants(pcSSAO); + buf.cmdDispatchThreadGroups( + { + .width = 1 + (uint32_t)sizeFb.width / 16, + .height = 1 + (uint32_t)sizeFb.height / 16, + }, + { .textures = { + lvk::TextureHandle(offscreenDepth), + lvk::TextureHandle(texSSAO), + } }); + + // 3. Blur SSAO + if (enableBlur) { + const lvk::Dimensions blurDim = { + .width = 1 + (uint32_t)sizeFb.width / 16, + .height = 1 + (uint32_t)sizeFb.height / 16, + }; + struct BlurPC { + uint32_t texDepth; + uint32_t texIn; + uint32_t texOut; + float depthThreshold; + }; + struct BlurPass { + lvk::TextureHandle texIn; + lvk::TextureHandle texOut; + }; + std::vector<BlurPass> passes; + { + passes.reserve(2 * numBlurPasses); + passes.push_back({ texSSAO, texBlur[0] }); + for (int i = 0; i != numBlurPasses - 1; i++) { + passes.push_back({ texBlur[0], texBlur[1] }); + passes.push_back({ texBlur[1], texBlur[0] }); + } + passes.push_back({ texBlur[0], texSSAO }); + } + for (uint32_t i = 0; i != passes.size(); i++) { + const BlurPass p = passes[i]; + buf.cmdBindComputePipeline(i & 1 ? pipelineBlurX : pipelineBlurY); + buf.cmdPushConstants(BlurPC{ + .texDepth = offscreenDepth.index(), + .texIn = p.texIn.index(), + .texOut = p.texOut.index(), + .depthThreshold = pcSSAO.zFar * depthThreshold, + }); + buf.cmdDispatchThreadGroups( + blurDim, { + .textures = {p.texIn, p.texOut, lvk::TextureHandle(offscreenDepth)} + }); + } + } + + // 3. Render scene with SSAO into the swapchain image + if (drawMode == DrawMode_SSAO) { + buf.cmdCopyImage(texSSAO, ctx->getCurrentSwapchainTexture(), ctx->getDimensions(offscreenColor)); + } else if (drawMode == DrawMode_Color) { + buf.cmdCopyImage(offscreenColor, ctx->getCurrentSwapchainTexture(), ctx->getDimensions(offscreenColor)); + } + + const lvk::RenderPass renderPassMain = { + .color = { { .loadOp = lvk::LoadOp_Load, .clearColor = { 1.0f, 1.0f, 1.0f, 1.0f } } }, + }; + const lvk::Framebuffer framebufferMain = { + .color = { { .texture = ctx->getCurrentSwapchainTexture() } }, + }; + + buf.cmdBeginRendering(renderPassMain, framebufferMain, { .textures = { lvk::TextureHandle(texSSAO) } }); + + if (drawMode == DrawMode_ColorSSAO) { + buf.cmdBindRenderPipeline(pipelineCombine); + buf.cmdPushConstants(pcCombine); + buf.cmdBindDepthState({}); + buf.cmdDraw(3); + } + + app.imgui_->beginFrame(framebufferMain); + app.drawFPS(); + app.drawMemo(); + + // render UI + { + const ImGuiViewport* v = ImGui::GetMainViewport(); + const float windowWidth = v->WorkSize.x / 5; + ImGui::SetNextWindowPos(ImVec2(10, 200)); + ImGui::SetNextWindowSize(ImVec2(windowWidth, v->WorkSize.y - 210)); + ImGui::Begin( + "SSAO", nullptr, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize); + ImGui::Checkbox("Draw wireframe", &drawWireframe); + ImGui::Checkbox("Enable blur", &enableBlur); + ImGui::BeginDisabled(!enableBlur); + ImGui::SliderFloat("Blur depth threshold", &depthThreshold, 0.0f, 50.0f); + ImGui::SliderInt("Blur num passes", &numBlurPasses, 1, 5); + ImGui::EndDisabled(); + ImGui::Text("Draw mode:"); + const float indentSize = 16.0f; + ImGui::Indent(indentSize); + ImGui::RadioButton("Color + SSAO", &drawMode, DrawMode_ColorSSAO); + ImGui::RadioButton("Color only", &drawMode, DrawMode_Color); + ImGui::RadioButton("SSAO only", &drawMode, DrawMode_SSAO); + ImGui::Unindent(indentSize); + ImGui::Separator(); + ImGui::BeginDisabled(drawMode != DrawMode_ColorSSAO); + ImGui::SliderFloat("SSAO scale", &pcCombine.scale, 0.0f, 2.0f); + ImGui::SliderFloat("SSAO bias", &pcCombine.bias, 0.0f, 0.3f); + ImGui::EndDisabled(); + ImGui::Separator(); + ImGui::BeginDisabled(drawMode == DrawMode_Color); + ImGui::SliderFloat("SSAO radius", &pcSSAO.radius, 0.01f, 0.1f); + ImGui::SliderFloat("SSAO attenuation scale", &pcSSAO.attScale, 0.5f, 1.5f); + ImGui::SliderFloat("SSAO distance scale", &pcSSAO.distScale, 0.0f, 2.0f); + ImGui::EndDisabled(); + ImGui::Separator(); + ImGui::Image(texSSAO.index(), ImVec2(windowWidth, windowWidth / aspectRatio)); + ImGui::End(); + } + + app.imgui_->endFrame(buf); + + buf.cmdEndRendering(); + } + ctx->submit(buf, ctx->getCurrentSwapchainTexture()); + }); + + ctx.release(); + + return 0; +} diff --git a/Chapter10/05_HDR/CMakeLists.txt b/Chapter10/05_HDR/CMakeLists.txt new file mode 100644 index 0000000..5f71634 --- /dev/null +++ b/Chapter10/05_HDR/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.16) + +project(Chapter10) + +include(../../CMake/CommonMacros.txt) + +SETUP_APP(Ch10_Sample05_HDR "Chapter 10") + +target_link_libraries(Ch10_Sample05_HDR PRIVATE SharedUtils assimp bc7enc meshoptimizer) diff --git a/Chapter10/05_HDR/src/Bloom.comp b/Chapter10/05_HDR/src/Bloom.comp new file mode 100644 index 0000000..3506e7e --- /dev/null +++ b/Chapter10/05_HDR/src/Bloom.comp @@ -0,0 +1,68 @@ +// +layout (local_size_x = 16, local_size_y = 16) in; + +layout (set = 0, binding = 0) uniform texture2D kTextures2D[]; +layout (set = 0, binding = 1) uniform sampler kSamplers[]; + +layout (set = 0, binding = 2, rgba8) uniform writeonly image2D kTextures2DOut[]; + +layout(push_constant) uniform PushConstants { + uint texIn; + uint texOut; + uint smpl; +} pc; + +ivec2 textureBindlessSize2D(uint textureid) { + return textureSize(nonuniformEXT(kTextures2D[textureid]), 0); +} + +vec4 textureBindless2D(uint textureid, vec2 uv) { + return textureLod(nonuniformEXT(sampler2D(kTextures2D[textureid], kSamplers[pc.smpl])), uv, 0); +} + +layout (constant_id = 0) const bool kIsHorizontal = true; + +const int kFilterSize = 17; + +// https://drdesten.github.io/web/tools/gaussian_kernel/ +const float gaussWeights[kFilterSize] = float[]( + 0.00001525878906, + 0.0002441406250, + 0.001831054688, + 0.008544921875, + 0.02777099609, + 0.06665039063, + 0.1221923828, + 0.1745605469, + 0.1963806152, + 0.1745605469, + 0.1221923828, + 0.06665039063, + 0.02777099609, + 0.008544921875, + 0.001831054688, + 0.0002441406250, + 0.00001525878906 +); + +void main() { + const vec2 size = textureBindlessSize2D(pc.texIn).xy; + const vec2 xy = gl_GlobalInvocationID.xy; + + if (xy.x > size.x || xy.y > size.y) + return; + + const vec2 texCoord = (gl_GlobalInvocationID.xy + vec2(0.5)) / size; + + const float texScaler = 1.0 / (kIsHorizontal ? size.x : size.y); + + vec3 c = vec3(0.0); + + for ( int i = 0; i != kFilterSize; i++ ) { + float offset = float(i - kFilterSize/2); + vec2 uv = texCoord + texScaler * (kIsHorizontal ? vec2(offset, 0) : vec2(0, offset)); + c += textureBindless2D(pc.texIn, uv).rgb * gaussWeights[i]; + } + + imageStore(kTextures2DOut[pc.texOut], ivec2(xy), vec4(c, 1.0) ); +} diff --git a/Chapter10/05_HDR/src/BrightPass.comp b/Chapter10/05_HDR/src/BrightPass.comp new file mode 100644 index 0000000..ce5c649 --- /dev/null +++ b/Chapter10/05_HDR/src/BrightPass.comp @@ -0,0 +1,54 @@ +// +layout (local_size_x = 16, local_size_y = 16) in; + +layout (set = 0, binding = 0) uniform texture2D kTextures2D[]; +layout (set = 0, binding = 1) uniform sampler kSamplers[]; + +layout (set = 0, binding = 2, rgba16) uniform writeonly image2D kTextures2DOutRGBA[]; +layout (set = 0, binding = 2, r16) uniform writeonly image2D kTextures2DOutR[]; + +layout(push_constant) uniform PushConstants { + uint texColor; + uint texOut; // rgba16 + uint texLuminance; // r16 + uint smpl; + float exposure; +} pc; + +ivec2 textureBindlessSize2D(uint textureid) { + return textureSize(nonuniformEXT(kTextures2D[textureid]), 0); +} + +vec4 textureBindless2D(uint textureid, vec2 uv) { + return textureLod(nonuniformEXT(sampler2D(kTextures2D[textureid], kSamplers[pc.smpl])), uv, 0); +} + +void main() { + const vec2 sizeIn = textureBindlessSize2D(pc.texColor).xy; + const vec2 sizeOut = textureBindlessSize2D(pc.texOut).xy; + + const vec2 xy = gl_GlobalInvocationID.xy; + const vec2 uv0 = (gl_GlobalInvocationID.xy + vec2(0)) / sizeOut; + const vec2 uv1 = (gl_GlobalInvocationID.xy + vec2(1)) / sizeOut; + + if (xy.x > sizeIn.x || xy.y > sizeIn.y) + return; + + vec2 dxdy = (uv1-uv0) / 3; + + vec4 color = vec4(0); + + // 3x3 box filter + for (int v = 0; v != 3; v++) { + for (int u = 0; u != 3; u++) { + color += textureBindless2D(pc.texColor, uv0 + vec2(u, v) * dxdy ); + } + } + + float luminance = pc.exposure * dot(color.rgb / 9, vec3(0.2126, 0.7152, 0.0722)); + + vec3 rgb = luminance > 1.0 ? color.rgb : vec3(0); + + imageStore(kTextures2DOutRGBA[pc.texOut], ivec2(xy), vec4( rgb, 1.0 ) ); + imageStore(kTextures2DOutR[pc.texLuminance], ivec2(xy), vec4(luminance ) ); +} diff --git a/Chapter10/05_HDR/src/ToneMap.frag b/Chapter10/05_HDR/src/ToneMap.frag new file mode 100644 index 0000000..48875ea --- /dev/null +++ b/Chapter10/05_HDR/src/ToneMap.frag @@ -0,0 +1,113 @@ +// +layout (location=0) in vec2 uv; +layout (location=0) out vec4 out_FragColor; + +const int ToneMappingMode_None = 0; +const int ToneMappingMode_Reinhard = 1; +const int ToneMappingMode_Uchimura = 2; +const int ToneMappingMode_KhronosPBR = 3; + +layout(push_constant) uniform PushConstants { + uint texColor; + uint texLuminance; + uint texBloom; + uint smpl; + int drawMode; + + float exposure; + float bloomStrength; + + // Reinhard + float maxWhite; + + // Uchimura + float P; // max display brightness + float a; // contrast + float m; // linear section start + float l; // linear section length + float c; // black tightness + float b; // pedestal + + // Khronos PBR + float startCompression; // highlight compression start + float desaturation; // desaturation speed +} pc; + +// Uchimura 2017, "HDR theory and practice" +// http://cdn2.gran-turismo.com/data/www/pdi_publications/PracticalHDRandWCGinGTS_20181222.pdf +// Math: https://www.desmos.com/calculator/gslcdxvipg +// Source: https://www.slideshare.net/nikuque/hdr-theory-and-practicce-jp +vec3 uchimura(vec3 x, float P, float a, float m, float l, float c, float b) { + float l0 = ((P - m) * l) / a; + float L0 = m - m / a; + float L1 = m + (1.0 - m) / a; + float S0 = m + l0; + float S1 = m + a * l0; + float C2 = (a * P) / (P - S1); + float CP = -C2 / P; + + vec3 w0 = vec3(1.0 - smoothstep(0.0, m, x)); + vec3 w2 = vec3(step(m + l0, x)); + vec3 w1 = vec3(1.0 - w0 - w2); + + vec3 T = vec3(m * pow(x / m, vec3(c)) + b); + vec3 S = vec3(P - (P - S1) * exp(CP * (x - S0))); + vec3 L = vec3(m + a * (x - m)); + + return T * w0 + L * w1 + S * w2; +} + +float luminance(vec3 v) { + return dot(v, vec3(0.2126, 0.7152, 0.0722)); +} + +// "Tone Mapping" by Matt Taylor: https://64.github.io/tonemapping/ +vec3 reinhard2(vec3 v, float maxWhite) { + float l_old = luminance(v); + float l_new = l_old * (1.0 + (l_old / (maxWhite * maxWhite))) / (1.0 + l_old); + return v * (l_new / l_old); +} + +// Khronos PBR Neutral Tone Mapper: +// https://github.com/KhronosGroup/ToneMapping/blob/main/PBR_Neutral/README.md#pbr-neutral-specification +// https://github.com/KhronosGroup/ToneMapping/blob/main/PBR_Neutral/pbrNeutral.glsl +vec3 PBRNeutralToneMapping(vec3 color, float startCompression, float desaturation) { + startCompression -= 0.04; + + float x = min(color.r, min(color.g, color.b)); + float offset = x < 0.08 ? x - 6.25 * x * x : 0.04; + color -= offset; + + float peak = max(color.r, max(color.g, color.b)); + if (peak < startCompression) return color; + + const float d = 1. - startCompression; + float newPeak = 1. - d * d / (peak + d - startCompression); + color *= newPeak / peak; + + float g = 1. - 1. / (desaturation * (peak - newPeak) + 1.); + return mix(color, newPeak * vec3(1, 1, 1), g); +} + +void main() { + vec3 color = textureBindless2D(pc.texColor, pc.smpl, uv).rgb; + vec3 bloom = textureBindless2D(pc.texBloom, pc.smpl, uv).rgb; + float avgLuminance = textureBindless2D(pc.texLuminance, pc.smpl, vec2(0.5)).r; + + if (pc.drawMode != ToneMappingMode_None) { + float midGray = 0.5; + color *= pc.exposure * midGray / (avgLuminance + 0.001); + } + + if (pc.drawMode == ToneMappingMode_Reinhard) { + color = reinhard2(pc.exposure * color, pc.maxWhite); + } + if (pc.drawMode == ToneMappingMode_Uchimura) { + color = uchimura(pc.exposure * color, pc.P, pc.a, pc.m, pc.l, pc.c, pc.b); + } + if (pc.drawMode == ToneMappingMode_KhronosPBR) { + color = PBRNeutralToneMapping(pc.exposure * color, pc.startCompression, pc.desaturation); + } + + out_FragColor = vec4(color + pc.bloomStrength * bloom, 1.0); +} diff --git a/Chapter10/05_HDR/src/main.cpp b/Chapter10/05_HDR/src/main.cpp new file mode 100644 index 0000000..c57b3ea --- /dev/null +++ b/Chapter10/05_HDR/src/main.cpp @@ -0,0 +1,445 @@ +#include "shared/VulkanApp.h" + +#include "Chapter10/Bistro.h" +#include "Chapter10/Skybox.h" + +#include <implot/implot.h> + +// Uchimura 2017, "HDR theory and practice" +// Math: https://www.desmos.com/calculator/gslcdxvipg +// Source: https://www.slideshare.net/nikuque/hdr-theory-and-practicce-jp +float uchimura(float x, float P, float a, float m, float l, float c, float b) +{ + float l0 = ((P - m) * l) / a; + float L0 = m - m / a; + float L1 = m + (1.0f - m) / a; + float S0 = m + l0; + float S1 = m + a * l0; + float C2 = (a * P) / (P - S1); + float CP = -C2 / P; + + float w0 = float(1.0f - glm::smoothstep(0.0f, m, x)); + float w2 = float(glm::step(m + l0, x)); + float w1 = float(1.0f - w0 - w2); + + float T = float(m * pow(x / m, float(c)) + b); + float S = float(P - (P - S1) * exp(CP * (x - S0))); + float L = float(m + a * (x - m)); + + return T * w0 + L * w1 + S * w2; +} + +float reinhard2(float v, float maxWhite) +{ + return v * (1.0f + (v / (maxWhite * maxWhite))) / (1.0f + v); +} + +// Khronos PBR Neutral Tone Mapper +// https://github.com/KhronosGroup/ToneMapping/blob/main/PBR_Neutral/README.md#pbr-neutral-specification +// https://github.com/KhronosGroup/ToneMapping/blob/main/PBR_Neutral/pbrNeutral.glsl +float PBRNeutralToneMapping(float color, float startCompression, float desaturation) +{ + startCompression -= 0.04f; + + float x = color; + float offset = x < 0.08f ? x - 6.25f * x * x : 0.04f; + color -= offset; + + float peak = color; + if (peak < startCompression) + return color; + + const float d = 1. - startCompression; + float newPeak = 1. - d * d / (peak + d - startCompression); + color *= newPeak / peak; + + float g = 1.0f - 1.0f / (desaturation * (peak - newPeak) + 1.0f); + return glm::mix(color, newPeak, g); +} + +int main() +{ + MeshData meshData; + Scene scene; + loadBistro(meshData, scene); + + VulkanApp app({ + .initialCameraPos = vec3(-19.261f, 8.465f, -7.317f), + .initialCameraTarget = vec3(0, +2.5f, 0), + }); + + app.positioner_.maxSpeed_ = 1.5f; + + std::unique_ptr<lvk::IContext> ctx(app.ctx_.get()); + + const uint32_t kNumSamples = 8; + const lvk::Format kOffscreenFormat = lvk::Format_RGBA_F16; + + const Skybox skyBox( + ctx, "data/immenstadter_horn_2k_prefilter.ktx", "data/immenstadter_horn_2k_irradiance.ktx", kOffscreenFormat, app.getDepthFormat(), + kNumSamples); + const VKMesh mesh(ctx, meshData, scene, kOffscreenFormat, app.getDepthFormat(), kNumSamples); + + lvk::Holder<lvk::ShaderModuleHandle> compBrightPass = loadShaderModule(ctx, "Chapter10/05_HDR/src/BrightPass.comp"); + lvk::Holder<lvk::ComputePipelineHandle> pipelineBrightPass = ctx->createComputePipeline({ .smComp = compBrightPass }); + + const uint32_t kHorizontal = 1; + const uint32_t kVertical = 0; + lvk::Holder<lvk::ShaderModuleHandle> compBloomPass = loadShaderModule(ctx, "Chapter10/05_HDR/src/Bloom.comp"); + lvk::Holder<lvk::ComputePipelineHandle> pipelineBloomX = ctx->createComputePipeline({ + .smComp = compBloomPass, + .specInfo = {.entries = { { .constantId = 0, .size = sizeof(uint32_t) } }, .data = &kHorizontal, .dataSize = sizeof(uint32_t)}, + }); + lvk::Holder<lvk::ComputePipelineHandle> pipelineBloomY = ctx->createComputePipeline({ + .smComp = compBloomPass, + .specInfo = {.entries = { { .constantId = 0, .size = sizeof(uint32_t) } }, .data = &kVertical, .dataSize = sizeof(uint32_t)}, + }); + + lvk::Holder<lvk::ShaderModuleHandle> vertToneMap = loadShaderModule(ctx, "data/shaders/QuadFlip.vert"); + lvk::Holder<lvk::ShaderModuleHandle> fragToneMap = loadShaderModule(ctx, "Chapter10/05_HDR/src/ToneMap.frag"); + + lvk::Holder<lvk::RenderPipelineHandle> pipelineToneMap = ctx->createRenderPipeline({ + .smVert = vertToneMap, + .smFrag = fragToneMap, + .color = { { .format = ctx->getSwapchainFormat() } }, + }); + + lvk::Holder<lvk::SamplerHandle> samplerClamp = ctx->createSampler({ + .wrapU = lvk::SamplerWrap_Clamp, + .wrapV = lvk::SamplerWrap_Clamp, + .wrapW = lvk::SamplerWrap_Clamp, + }); + + const lvk::Dimensions sizeFb = ctx->getDimensions(ctx->getCurrentSwapchainTexture()); + + lvk::Holder<lvk::TextureHandle> msaaColor = ctx->createTexture({ + .format = kOffscreenFormat, + .dimensions = sizeFb, + .numSamples = kNumSamples, + .usage = lvk::TextureUsageBits_Attachment, + .debugName = "msaaColor", + }); + lvk::Holder<lvk::TextureHandle> msaaDepth = ctx->createTexture({ + .format = app.getDepthFormat(), + .dimensions = sizeFb, + .numSamples = kNumSamples, + .usage = lvk::TextureUsageBits_Attachment, + .debugName = "msaaDepth", + }); + + const lvk::Dimensions sizeBloom = { 512, 512 }; + + lvk::Holder<lvk::TextureHandle> texBrightPass = ctx->createTexture({ + .format = kOffscreenFormat, + .dimensions = sizeBloom, + .usage = lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .debugName = "texBrightPass", + }); + lvk::Holder<lvk::TextureHandle> texBloomPass = ctx->createTexture({ + .format = kOffscreenFormat, + .dimensions = sizeBloom, + .usage = lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .debugName = "texBloomPass", + }); + + const lvk::ComponentMapping swizzle = { .r = lvk::Swizzle_R, .g = lvk::Swizzle_R, .b = lvk::Swizzle_R, .a = lvk::Swizzle_1 }; + + lvk::Holder<lvk::TextureHandle> texLuminanceViews[10] = { ctx->createTexture({ + .format = lvk::Format_R_F16, + .dimensions = sizeBloom, + .usage = lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .numMipLevels = lvk::calcNumMipLevels(sizeBloom.width, sizeBloom.height), + .swizzle = swizzle, + .debugName = "texLuminance", + }) }; + + for (uint32_t l = 1; l != LVK_ARRAY_NUM_ELEMENTS(texLuminanceViews); l++) { + texLuminanceViews[l] = ctx->createTextureView(texLuminanceViews[0], { .mipLevel = l, .swizzle = swizzle }); + } + + lvk::Holder<lvk::TextureHandle> offscreenColor = ctx->createTexture({ + .format = kOffscreenFormat, + .dimensions = sizeFb, + .usage = lvk::TextureUsageBits_Attachment | lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .debugName = "offscreenColor", + }); + + lvk::Holder<lvk::TextureHandle> texBloom[] = { + ctx->createTexture({ + .format = kOffscreenFormat, + .dimensions = sizeBloom, + .usage = lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .debugName = "texBloom0", + }), + ctx->createTexture({ + .format = kOffscreenFormat, + .dimensions = sizeBloom, + .usage = lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .debugName = "texBloom1", + }), + }; + + bool drawWireframe = false; + bool drawCurves = true; + bool enableBloom = true; + float bloomStrength = 0.1f; + int numBloomPasses = 2; + + enum ToneMappingMode { + ToneMapping_None = 0, + ToneMapping_Reinhard = 1, + ToneMapping_Uchimura = 2, + ToneMapping_KhronosPBR = 3, + }; + + ImPlotContext* implotCtx = ImPlot::CreateContext(); + + struct { + uint32_t texColor; + uint32_t texLuminance; + uint32_t texBloom; + uint32_t sampler; + int drawMode = ToneMapping_Uchimura; + + float exposure = 1.0f; + float bloomStrength = 0.1f; + + // Reinhard + float maxWhite = 1.0f; + + // Uchimura + float P = 1.0f; // max display brightness + float a = 1.05f; // contrast + float m = 0.1f; // linear section start + float l = 0.8f; // linear section length + float c = 3.0f; // black tightness + float b = 0.0f; // pedestal + + // Khronos PBR + float startCompression = 0.8f; // highlight compression start + float desaturation = 0.15f; // desaturation speed + } pcHDR = { + .texColor = offscreenColor.index(), + .texLuminance = texLuminanceViews[LVK_ARRAY_NUM_ELEMENTS(texLuminanceViews) - 1].index(), // 1x1 + .texBloom = texBloomPass.index(), + .sampler = samplerClamp.index(), + }; + + app.run([&](uint32_t width, uint32_t height, float aspectRatio, float deltaSeconds) { + const mat4 view = app.camera_.getViewMatrix(); + const mat4 proj = glm::perspective(45.0f, aspectRatio, 0.01f, 1000.0f); + + lvk::ICommandBuffer& buf = ctx->acquireCommandBuffer(); + { + // 1. Render scene + buf.cmdBeginRendering( + lvk::RenderPass{ + .color = { { .loadOp = lvk::LoadOp_Clear, .storeOp = lvk::StoreOp_MsaaResolve, .clearColor = { 1.0f, 1.0f, 1.0f, 1.0f } } }, + .depth = { .loadOp = lvk::LoadOp_Clear, .clearDepth = 1.0f } + }, + lvk::Framebuffer{ + .color = { { .texture = msaaColor, .resolveTexture = offscreenColor } }, + .depthStencil = { .texture = msaaDepth }, + }); + skyBox.draw(buf, view, proj); + { + buf.cmdPushDebugGroupLabel("Mesh", 0xff0000ff); + mesh.draw(*ctx.get(), buf, view, proj, skyBox.texSkyboxIrradiance, drawWireframe); + buf.cmdPopDebugGroupLabel(); + } + app.drawGrid(buf, proj, vec3(0, -1.0f, 0), kNumSamples, kOffscreenFormat); + buf.cmdEndRendering(); + + // 2. Bright pass - extract luminance and bright areas + const struct { + uint32_t texColor; + uint32_t texOut; + uint32_t texLuminance; + uint32_t sampler; + float exposure; + } pcBrightPass = { + .texColor = offscreenColor.index(), + .texOut = texBrightPass.index(), + .texLuminance = texLuminanceViews[0].index(), + .sampler = samplerClamp.index(), + .exposure = pcHDR.exposure, + }; + buf.cmdBindComputePipeline(pipelineBrightPass); + buf.cmdPushConstants(pcBrightPass); + buf.cmdDispatchThreadGroups( + sizeBloom.divide2D(16), { + .textures = {lvk::TextureHandle(offscreenColor), lvk::TextureHandle(texLuminanceViews[0])} + }); + buf.cmdGenerateMipmap(texLuminanceViews[0]); + + // 2.1. Bloom + struct BlurPC { + uint32_t texIn; + uint32_t texOut; + uint32_t sampler; + }; + struct StreaksPC { + uint32_t texIn; + uint32_t texOut; + uint32_t texRotationPattern; + uint32_t sampler; + }; + struct BlurPass { + lvk::TextureHandle texIn; + lvk::TextureHandle texOut; + }; + std::vector<BlurPass> passes; + { + passes.reserve(2 * numBloomPasses); + passes.push_back({ texBrightPass, texBloom[0] }); + for (int i = 0; i != numBloomPasses - 1; i++) { + passes.push_back({ texBloom[0], texBloom[1] }); + passes.push_back({ texBloom[1], texBloom[0] }); + } + passes.push_back({ texBloom[0], texBloomPass }); + } + for (uint32_t i = 0; i != passes.size(); i++) { + const BlurPass p = passes[i]; + buf.cmdBindComputePipeline(i & 1 ? pipelineBloomX : pipelineBloomY); + buf.cmdPushConstants(BlurPC{ + .texIn = p.texIn.index(), + .texOut = p.texOut.index(), + .sampler = samplerClamp.index(), + }); + if (enableBloom) + buf.cmdDispatchThreadGroups( + sizeBloom.divide2D(16), { + .textures = {p.texIn, p.texOut, lvk::TextureHandle(texBrightPass)} + }); + } + + // 3. Render tone-mapped scene into a swapchain image + const lvk::RenderPass renderPassMain = { + .color = { { .loadOp = lvk::LoadOp_Load, .clearColor = { 1.0f, 1.0f, 1.0f, 1.0f } } }, + }; + const lvk::Framebuffer framebufferMain = { + .color = { { .texture = ctx->getCurrentSwapchainTexture() } }, + }; + + // transition the entire mip-pyramid + buf.cmdBeginRendering(renderPassMain, framebufferMain, { .textures = { lvk::TextureHandle(texLuminanceViews[0]) } }); + + buf.cmdBindRenderPipeline(pipelineToneMap); + buf.cmdPushConstants(pcHDR); + buf.cmdBindDepthState({}); + buf.cmdDraw(3); // fullscreen triangle + + app.imgui_->beginFrame(framebufferMain); + app.drawFPS(); + app.drawMemo(); + + // render UI + { + const ImGuiViewport* v = ImGui::GetMainViewport(); + const float windowWidth = v->WorkSize.x / 5; + ImGui::SetNextWindowPos(ImVec2(10, 200)); + ImGui::SetNextWindowSize(ImVec2(windowWidth, v->WorkSize.y - 210)); + ImGui::Begin("HDR", nullptr, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize); + ImGui::Checkbox("Draw wireframe", &drawWireframe); + ImGui::Checkbox("Draw tone mapping curves", &drawCurves); + ImGui::Separator(); + const float indentSize = 32.0f; + ImGui::Text("Tone mapping params:"); + ImGui::SliderFloat("Exposure", &pcHDR.exposure, 0.1f, 2.0f); + ImGui::Checkbox("Enable bloom", &enableBloom); + pcHDR.bloomStrength = enableBloom ? bloomStrength : 0.0f; + ImGui::BeginDisabled(!enableBloom); + ImGui::Indent(indentSize); + ImGui::SliderFloat("Bloom strength", &bloomStrength, 0.0f, 1.0f); + ImGui::SliderInt("Bloom num passes", &numBloomPasses, 1, 5); + ImGui::Unindent(indentSize); + ImGui::EndDisabled(); + ImGui::Text("Tone mapping mode:"); + ImGui::RadioButton("None", &pcHDR.drawMode, ToneMapping_None); + ImGui::RadioButton("Reinhard", &pcHDR.drawMode, ToneMapping_Reinhard); + if (pcHDR.drawMode == ToneMapping_Reinhard) { + ImGui::Indent(indentSize); + ImGui::BeginDisabled(pcHDR.drawMode != ToneMapping_Reinhard); + ImGui::SliderFloat("Max white", &pcHDR.maxWhite, 0.5f, 2.0f); + ImGui::EndDisabled(); + ImGui::Unindent(indentSize); + } + ImGui::RadioButton("Uchimura", &pcHDR.drawMode, ToneMapping_Uchimura); + if (pcHDR.drawMode == ToneMapping_Uchimura) { + ImGui::Indent(indentSize); + ImGui::BeginDisabled(pcHDR.drawMode != ToneMapping_Uchimura); + ImGui::SliderFloat("Max brightness", &pcHDR.P, 1.0f, 2.0f); + ImGui::SliderFloat("Contrast", &pcHDR.a, 0.0f, 5.0f); + ImGui::SliderFloat("Linear section start", &pcHDR.m, 0.0f, 1.0f); + ImGui::SliderFloat("Linear section length", &pcHDR.l, 0.0f, 1.0f); + ImGui::SliderFloat("Black tightness", &pcHDR.c, 1.0f, 3.0f); + ImGui::SliderFloat("Pedestal", &pcHDR.b, 0.0f, 1.0f); + ImGui::EndDisabled(); + ImGui::Unindent(indentSize); + } + ImGui::RadioButton("Khronos PBR Neutral", &pcHDR.drawMode, ToneMapping_KhronosPBR); + if (pcHDR.drawMode == ToneMapping_KhronosPBR) { + ImGui::Indent(indentSize); + ImGui::SliderFloat("Highlight compression start", &pcHDR.startCompression, 0.0f, 1.0f); + ImGui::SliderFloat("Desaturation speed", &pcHDR.desaturation, 0.0f, 1.0f); + ImGui::Unindent(indentSize); + } + ImGui::Separator(); + + ImGui::Text("Average luminance 1x1:"); + ImGui::Image(pcHDR.texLuminance, ImVec2(128, 128)); + ImGui::Separator(); + ImGui::Text("Bright pass:"); + ImGui::Image(texBrightPass.index(), ImVec2(windowWidth, windowWidth / aspectRatio)); + ImGui::Text("Bloom pass:"); + ImGui::Image(texBloomPass.index(), ImVec2(windowWidth, windowWidth / aspectRatio)); + ImGui::Separator(); + ImGui::Text("Luminance pyramid 512x512"); + for (uint32_t l = 0; l != LVK_ARRAY_NUM_ELEMENTS(texLuminanceViews); l++) { + ImGui::Image(texLuminanceViews[l].index(), ImVec2((int)windowWidth >> l, ((int)windowWidth >> l))); + } + ImGui::Separator(); + ImGui::End(); + + if (drawCurves) { + const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; + ImGui::SetNextWindowBgAlpha(0.8f); + ImGui::SetNextWindowPos({ width * 0.6f, height * 0.7f }, ImGuiCond_Appearing); + ImGui::SetNextWindowSize({ width * 0.4f, height * 0.3f }); + ImGui::Begin("Tone mapping curve", nullptr, flags); + const int kNumGraphPoints = 1001; + float xs[kNumGraphPoints]; + float ysUnchimura[kNumGraphPoints]; + float ysReinhard2[kNumGraphPoints]; + float ysKhronosPBR[kNumGraphPoints]; + for (int i = 0; i != kNumGraphPoints; i++) { + xs[i] = float(i) / kNumGraphPoints; + ysUnchimura[i] = uchimura(xs[i], pcHDR.P, pcHDR.a, pcHDR.m, pcHDR.l, pcHDR.c, pcHDR.b); + ysReinhard2[i] = reinhard2(xs[i], pcHDR.maxWhite); + ysKhronosPBR[i] = PBRNeutralToneMapping(xs[i], pcHDR.startCompression, pcHDR.desaturation); + } + if (ImPlot::BeginPlot("Tone mapping curves", { width * 0.4f, height * 0.3f }, ImPlotFlags_NoInputs)) { + ImPlot::SetupAxes("Input", "Output"); + ImPlot::PlotLine("Uchimura", xs, ysUnchimura, kNumGraphPoints); + ImPlot::PlotLine("Reinhard", xs, ysReinhard2, kNumGraphPoints); + ImPlot::PlotLine("Khronos PBR", xs, ysKhronosPBR, kNumGraphPoints); + ImPlot::EndPlot(); + } + ImGui::End(); + } + } + + app.imgui_->endFrame(buf); + + buf.cmdEndRendering(); + } + ctx->submit(buf, ctx->getCurrentSwapchainTexture()); + }); + + ImPlot::DestroyContext(implotCtx); + + ctx.release(); + + return 0; +} diff --git a/Chapter10/06_HDR_Adaptation/CMakeLists.txt b/Chapter10/06_HDR_Adaptation/CMakeLists.txt new file mode 100644 index 0000000..d5ef24e --- /dev/null +++ b/Chapter10/06_HDR_Adaptation/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.16) + +project(Chapter10) + +include(../../CMake/CommonMacros.txt) + +SETUP_APP(Ch10_Sample06_HDR_Adaptation "Chapter 10") + +target_link_libraries(Ch10_Sample06_HDR_Adaptation PRIVATE SharedUtils assimp bc7enc meshoptimizer) diff --git a/Chapter10/06_HDR_Adaptation/src/Adaptation.comp b/Chapter10/06_HDR_Adaptation/src/Adaptation.comp new file mode 100644 index 0000000..c1aef6c --- /dev/null +++ b/Chapter10/06_HDR_Adaptation/src/Adaptation.comp @@ -0,0 +1,21 @@ +/**/ +layout (local_size_x = 1, local_size_y = 1) in; + +layout (set = 0, binding = 2, r16) uniform readonly image2D kTextures2DIn[]; +layout (set = 0, binding = 2, r16) uniform writeonly image2D kTextures2DOut[]; + +layout(push_constant) uniform PushConstants { + uint texCurrSceneLuminance; + uint texPrevAdaptedLuminance; + uint texAdaptedOut; + float adaptationSpeed; +} pc; + +void main() { + float lumCurr = imageLoad(kTextures2DIn[pc.texCurrSceneLuminance ], ivec2(0, 0)).x; + float lumPrev = imageLoad(kTextures2DIn[pc.texPrevAdaptedLuminance], ivec2(0, 0)).x; + + float newAdaptation = lumPrev + (lumCurr - lumPrev) * (1.0 - pow(0.98, pc.adaptationSpeed)); + + imageStore(kTextures2DOut[pc.texAdaptedOut], ivec2(0, 0), vec4(newAdaptation)); +} diff --git a/Chapter10/06_HDR_Adaptation/src/main.cpp b/Chapter10/06_HDR_Adaptation/src/main.cpp new file mode 100644 index 0000000..d61f966 --- /dev/null +++ b/Chapter10/06_HDR_Adaptation/src/main.cpp @@ -0,0 +1,495 @@ +#include "shared/VulkanApp.h" + +#include "Chapter10/Bistro.h" +#include "Chapter10/Skybox.h" + +#include <implot/implot.h> + +// Uchimura 2017, "HDR theory and practice" +// Math: https://www.desmos.com/calculator/gslcdxvipg +// Source: https://www.slideshare.net/nikuque/hdr-theory-and-practicce-jp +float uchimura(float x, float P, float a, float m, float l, float c, float b) +{ + float l0 = ((P - m) * l) / a; + float L0 = m - m / a; + float L1 = m + (1.0f - m) / a; + float S0 = m + l0; + float S1 = m + a * l0; + float C2 = (a * P) / (P - S1); + float CP = -C2 / P; + + float w0 = float(1.0f - glm::smoothstep(0.0f, m, x)); + float w2 = float(glm::step(m + l0, x)); + float w1 = float(1.0f - w0 - w2); + + float T = float(m * pow(x / m, float(c)) + b); + float S = float(P - (P - S1) * exp(CP * (x - S0))); + float L = float(m + a * (x - m)); + + return T * w0 + L * w1 + S * w2; +} + +float reinhard2(float v, float maxWhite) +{ + return v * (1.0f + (v / (maxWhite * maxWhite))) / (1.0f + v); +} + +// Khronos PBR Neutral Tone Mapper +// https://github.com/KhronosGroup/ToneMapping/blob/main/PBR_Neutral/README.md#pbr-neutral-specification +// https://github.com/KhronosGroup/ToneMapping/blob/main/PBR_Neutral/pbrNeutral.glsl +float PBRNeutralToneMapping(float color, float startCompression, float desaturation) +{ + startCompression -= 0.04f; + + float x = color; + float offset = x < 0.08f ? x - 6.25f * x * x : 0.04f; + color -= offset; + + float peak = color; + if (peak < startCompression) + return color; + + const float d = 1. - startCompression; + float newPeak = 1. - d * d / (peak + d - startCompression); + color *= newPeak / peak; + + float g = 1.0f - 1.0f / (desaturation * (peak - newPeak) + 1.0f); + return glm::mix(color, newPeak, g); +} + +int main() +{ + MeshData meshData; + Scene scene; + loadBistro(meshData, scene); + + VulkanApp app({ + .initialCameraPos = vec3(-19.261f, 8.465f, -7.317f), + .initialCameraTarget = vec3(0, +2.5f, 0), + }); + + app.positioner_.maxSpeed_ = 1.5f; + + std::unique_ptr<lvk::IContext> ctx(app.ctx_.get()); + + const uint32_t kNumSamples = 8; + const lvk::Format kOffscreenFormat = lvk::Format_RGBA_F16; + + const Skybox skyBox( + ctx, "data/immenstadter_horn_2k_prefilter.ktx", "data/immenstadter_horn_2k_irradiance.ktx", kOffscreenFormat, app.getDepthFormat(), + kNumSamples); + const VKMesh mesh(ctx, meshData, scene, kOffscreenFormat, app.getDepthFormat(), kNumSamples); + + lvk::Holder<lvk::ShaderModuleHandle> compBrightPass = loadShaderModule(ctx, "Chapter10/05_HDR/src/BrightPass.comp"); + lvk::Holder<lvk::ComputePipelineHandle> pipelineBrightPass = ctx->createComputePipeline({ .smComp = compBrightPass }); + + lvk::Holder<lvk::ShaderModuleHandle> compAdaptationPass = loadShaderModule(ctx, "Chapter10/06_HDR_Adaptation/src/Adaptation.comp"); + lvk::Holder<lvk::ComputePipelineHandle> pipelineAdaptationPass = ctx->createComputePipeline({ .smComp = compAdaptationPass }); + + const uint32_t kHorizontal = 1; + const uint32_t kVertical = 0; + + lvk::Holder<lvk::ShaderModuleHandle> compBloomPass = loadShaderModule(ctx, "Chapter10/05_HDR/src/Bloom.comp"); + lvk::Holder<lvk::ComputePipelineHandle> pipelineBloomX = ctx->createComputePipeline({ + .smComp = compBloomPass, + .specInfo = {.entries = { { .constantId = 0, .size = sizeof(uint32_t) } }, .data = &kHorizontal, .dataSize = sizeof(uint32_t)}, + }); + lvk::Holder<lvk::ComputePipelineHandle> pipelineBloomY = ctx->createComputePipeline({ + .smComp = compBloomPass, + .specInfo = {.entries = { { .constantId = 0, .size = sizeof(uint32_t) } }, .data = &kVertical, .dataSize = sizeof(uint32_t)}, + }); + + lvk::Holder<lvk::ShaderModuleHandle> vertToneMap = loadShaderModule(ctx, "data/shaders/QuadFlip.vert"); + lvk::Holder<lvk::ShaderModuleHandle> fragToneMap = loadShaderModule(ctx, "Chapter10/05_HDR/src/ToneMap.frag"); + + lvk::Holder<lvk::RenderPipelineHandle> pipelineToneMap = ctx->createRenderPipeline({ + .smVert = vertToneMap, + .smFrag = fragToneMap, + .color = { { .format = ctx->getSwapchainFormat() } }, + }); + + lvk::Holder<lvk::SamplerHandle> samplerClamp = ctx->createSampler({ + .wrapU = lvk::SamplerWrap_Clamp, + .wrapV = lvk::SamplerWrap_Clamp, + .wrapW = lvk::SamplerWrap_Clamp, + }); + + const lvk::Dimensions sizeFb = ctx->getDimensions(ctx->getCurrentSwapchainTexture()); + + lvk::Holder<lvk::TextureHandle> msaaColor = ctx->createTexture({ + .format = kOffscreenFormat, + .dimensions = sizeFb, + .numSamples = kNumSamples, + .usage = lvk::TextureUsageBits_Attachment, + .debugName = "msaaColor", + }); + lvk::Holder<lvk::TextureHandle> msaaDepth = ctx->createTexture({ + .format = app.getDepthFormat(), + .dimensions = sizeFb, + .numSamples = kNumSamples, + .usage = lvk::TextureUsageBits_Attachment, + .debugName = "msaaDepth", + }); + + const lvk::Dimensions sizeBloom = { 512, 512 }; + + lvk::Holder<lvk::TextureHandle> texBrightPass = ctx->createTexture({ + .format = kOffscreenFormat, + .dimensions = sizeBloom, + .usage = lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .debugName = "texBrightPass", + }); + lvk::Holder<lvk::TextureHandle> texBloomPass = ctx->createTexture({ + .format = kOffscreenFormat, + .dimensions = sizeBloom, + .usage = lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .debugName = "texBloomPass", + }); + + const lvk::ComponentMapping swizzle = { .r = lvk::Swizzle_R, .g = lvk::Swizzle_R, .b = lvk::Swizzle_R, .a = lvk::Swizzle_1 }; + + lvk::Holder<lvk::TextureHandle> texLuminanceViews[10] = { ctx->createTexture({ + .format = lvk::Format_R_F16, + .dimensions = sizeBloom, + .usage = lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .numMipLevels = lvk::calcNumMipLevels(sizeBloom.width, sizeBloom.height), + .swizzle = swizzle, + .debugName = "texLuminance", + }) }; + + for (uint32_t l = 1; l != LVK_ARRAY_NUM_ELEMENTS(texLuminanceViews); l++) { + texLuminanceViews[l] = ctx->createTextureView(texLuminanceViews[0], { .mipLevel = l, .swizzle = swizzle }); + } + + const uint16_t brightPixel = glm::packHalf1x16(50.0f); + + // ping-pong textures for iterative luminance adaptation + const lvk::TextureDesc luminanceTextureDesc{ + .format = lvk::Format_R_F16, + .dimensions = {1, 1}, + .usage = lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .swizzle = swizzle, + .data = &brightPixel, + }; + lvk::Holder<lvk::TextureHandle> texAdaptedLuminance[2] = { + ctx->createTexture(luminanceTextureDesc, "texAdaptedLuminance0"), + ctx->createTexture(luminanceTextureDesc, "texAdaptedLuminance1"), + }; + + lvk::Holder<lvk::TextureHandle> offscreenColor = ctx->createTexture({ + .format = kOffscreenFormat, + .dimensions = sizeFb, + .usage = lvk::TextureUsageBits_Attachment | lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .debugName = "offscreenColor", + }); + + lvk::Holder<lvk::TextureHandle> texBloom[] = { + ctx->createTexture({ + .format = kOffscreenFormat, + .dimensions = sizeBloom, + .usage = lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .debugName = "texBloom0", + }), + ctx->createTexture({ + .format = kOffscreenFormat, + .dimensions = sizeBloom, + .usage = lvk::TextureUsageBits_Sampled | lvk::TextureUsageBits_Storage, + .debugName = "texBloom1", + }), + }; + + bool drawWireframe = false; + bool drawCurves = false; + bool enableBloom = true; + float bloomStrength = 0.01f; + int numBloomPasses = 2; + float adaptationSpeed = 1.0f; + + enum ToneMappingMode { + ToneMapping_None = 0, + ToneMapping_Reinhard = 1, + ToneMapping_Uchimura = 2, + ToneMapping_KhronosPBR = 3, + }; + + ImPlotContext* implotCtx = ImPlot::CreateContext(); + + struct { + uint32_t texColor; + uint32_t texLuminance; + uint32_t texBloom; + uint32_t sampler; + int drawMode = ToneMapping_Uchimura; + + float exposure = 1.0f; + float bloomStrength = 0.1f; + + // Reinhard + float maxWhite = 1.0f; + + // Uchimura + float P = 1.0f; // max display brightness + float a = 1.05f; // contrast + float m = 0.1f; // linear section start + float l = 0.8f; // linear section length + float c = 3.0f; // black tightness + float b = 0.0f; // pedestal + + // Khronos PBR + float startCompression = 0.8f; // highlight compression start + float desaturation = 0.15f; // desaturation speed + } pcHDR = { + .texColor = offscreenColor.index(), + .texLuminance = texAdaptedLuminance[0].index(), // 1x1 + .texBloom = texBloomPass.index(), + .sampler = samplerClamp.index(), + }; + + app.run([&](uint32_t width, uint32_t height, float aspectRatio, float deltaSeconds) { + const mat4 view = app.camera_.getViewMatrix(); + const mat4 proj = glm::perspective(45.0f, aspectRatio, 0.01f, 1000.0f); + + lvk::ICommandBuffer& buf = ctx->acquireCommandBuffer(); + { + // 1. Render scene + buf.cmdBeginRendering( + lvk::RenderPass{ + .color = { { .loadOp = lvk::LoadOp_Clear, .storeOp = lvk::StoreOp_MsaaResolve, .clearColor = { 1.0f, 1.0f, 1.0f, 1.0f } } }, + .depth = { .loadOp = lvk::LoadOp_Clear, .clearDepth = 1.0f } + }, + lvk::Framebuffer{ + .color = { { .texture = msaaColor, .resolveTexture = offscreenColor } }, + .depthStencil = { .texture = msaaDepth }, + }); + skyBox.draw(buf, view, proj); + { + buf.cmdPushDebugGroupLabel("Mesh", 0xff0000ff); + mesh.draw(*ctx.get(), buf, view, proj, skyBox.texSkyboxIrradiance, drawWireframe); + buf.cmdPopDebugGroupLabel(); + } + app.drawGrid(buf, proj, vec3(0, -1.0f, 0), kNumSamples, kOffscreenFormat); + buf.cmdEndRendering(); + + // 2. Bright pass - extract luminance and bright areas + const struct { + uint32_t texColor; + uint32_t texOut; + uint32_t texLuminance; + uint32_t sampler; + float exposure; + } pcBrightPass = { + .texColor = offscreenColor.index(), + .texOut = texBrightPass.index(), + .texLuminance = texLuminanceViews[0].index(), + .sampler = samplerClamp.index(), + .exposure = pcHDR.exposure, + }; + buf.cmdBindComputePipeline(pipelineBrightPass); + buf.cmdPushConstants(pcBrightPass); + buf.cmdDispatchThreadGroups( + sizeBloom.divide2D(16), { + .textures = {lvk::TextureHandle(offscreenColor), lvk::TextureHandle(texLuminanceViews[0])} + }); + buf.cmdGenerateMipmap(texLuminanceViews[0]); + + // 2.1. Bloom + struct BlurPC { + uint32_t texIn; + uint32_t texOut; + uint32_t sampler; + }; + struct StreaksPC { + uint32_t texIn; + uint32_t texOut; + uint32_t texRotationPattern; + uint32_t sampler; + }; + struct BlurPass { + lvk::TextureHandle texIn; + lvk::TextureHandle texOut; + }; + std::vector<BlurPass> passes; + { + passes.reserve(2 * numBloomPasses); + passes.push_back({ texBrightPass, texBloom[0] }); + for (int i = 0; i != numBloomPasses - 1; i++) { + passes.push_back({ texBloom[0], texBloom[1] }); + passes.push_back({ texBloom[1], texBloom[0] }); + } + passes.push_back({ texBloom[0], texBloomPass }); + } + for (uint32_t i = 0; i != passes.size(); i++) { + const BlurPass p = passes[i]; + buf.cmdBindComputePipeline(i & 1 ? pipelineBloomX : pipelineBloomY); + buf.cmdPushConstants(BlurPC{ + .texIn = p.texIn.index(), + .texOut = p.texOut.index(), + .sampler = samplerClamp.index(), + }); + if (enableBloom) + buf.cmdDispatchThreadGroups( + sizeBloom.divide2D(16), { + .textures = {p.texIn, p.texOut, lvk::TextureHandle(texBrightPass)} + }); + } + + // 3. Light adaptation pass + const struct { + uint32_t texCurrSceneLuminance; + uint32_t texPrevAdaptedLuminance; + uint32_t texNewAdaptedLuminance; + float adaptationSpeed; + } pcAdaptationPass = { + .texCurrSceneLuminance = texLuminanceViews[LVK_ARRAY_NUM_ELEMENTS(texLuminanceViews) - 1].index(), // 1x1, + .texPrevAdaptedLuminance = texAdaptedLuminance[0].index(), + .texNewAdaptedLuminance = texAdaptedLuminance[1].index(), + .adaptationSpeed = 100.0f * deltaSeconds * adaptationSpeed, + }; + buf.cmdBindComputePipeline(pipelineAdaptationPass); + buf.cmdPushConstants(pcAdaptationPass); + buf.cmdDispatchThreadGroups( + { + 1, 1, 1 + }, + { .textures = { + lvk::TextureHandle(texLuminanceViews[0]), // transition the entire mip-pyramid + lvk::TextureHandle(texAdaptedLuminance[0]), + lvk::TextureHandle(texAdaptedLuminance[1]), + } }); + + // 4. Render tone-mapped scene into a swapchain image + const lvk::RenderPass renderPassMain = { + .color = { { .loadOp = lvk::LoadOp_Load, .clearColor = { 1.0f, 1.0f, 1.0f, 1.0f } } }, + }; + const lvk::Framebuffer framebufferMain = { + .color = { { .texture = ctx->getCurrentSwapchainTexture() } }, + }; + + // transition the entire mip-pyramid + buf.cmdBeginRendering(renderPassMain, framebufferMain, { .textures = { lvk::TextureHandle(texAdaptedLuminance[1]) } }); + + buf.cmdBindRenderPipeline(pipelineToneMap); + buf.cmdPushConstants(pcHDR); + buf.cmdBindDepthState({}); + buf.cmdDraw(3); // fullscreen triangle + + app.imgui_->beginFrame(framebufferMain); + app.drawFPS(); + app.drawMemo(); + + // render UI + { + const ImGuiViewport* v = ImGui::GetMainViewport(); + const float windowWidth = v->WorkSize.x / 5; + ImGui::SetNextWindowPos(ImVec2(10, 200)); + ImGui::SetNextWindowSize(ImVec2(windowWidth, v->WorkSize.y - 210)); + ImGui::Begin("HDR", nullptr, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize); + ImGui::Checkbox("Draw wireframe", &drawWireframe); + ImGui::Checkbox("Draw tone mapping curves", &drawCurves); + ImGui::Separator(); + const float indentSize = 32.0f; + ImGui::Text("Tone mapping params:"); + ImGui::SliderFloat("Exposure", &pcHDR.exposure, 0.1f, 2.0f); + ImGui::SliderFloat("Adaptation speed", &adaptationSpeed, 0.1f, 2.0f); + ImGui::Checkbox("Enable bloom", &enableBloom); + pcHDR.bloomStrength = enableBloom ? bloomStrength : 0.0f; + ImGui::BeginDisabled(!enableBloom); + ImGui::Indent(indentSize); + ImGui::SliderFloat("Bloom strength", &bloomStrength, 0.0f, 1.0f); + ImGui::SliderInt("Bloom num passes", &numBloomPasses, 1, 5); + ImGui::Unindent(indentSize); + ImGui::EndDisabled(); + ImGui::Text("Tone mapping mode:"); + ImGui::RadioButton("None", &pcHDR.drawMode, ToneMapping_None); + ImGui::RadioButton("Reinhard", &pcHDR.drawMode, ToneMapping_Reinhard); + if (pcHDR.drawMode == ToneMapping_Reinhard) { + ImGui::Indent(indentSize); + ImGui::BeginDisabled(pcHDR.drawMode != ToneMapping_Reinhard); + ImGui::SliderFloat("Max white", &pcHDR.maxWhite, 0.5f, 2.0f); + ImGui::EndDisabled(); + ImGui::Unindent(indentSize); + } + ImGui::RadioButton("Uchimura", &pcHDR.drawMode, ToneMapping_Uchimura); + if (pcHDR.drawMode == ToneMapping_Uchimura) { + ImGui::Indent(indentSize); + ImGui::BeginDisabled(pcHDR.drawMode != ToneMapping_Uchimura); + ImGui::SliderFloat("Max brightness", &pcHDR.P, 1.0f, 2.0f); + ImGui::SliderFloat("Contrast", &pcHDR.a, 0.0f, 5.0f); + ImGui::SliderFloat("Linear section start", &pcHDR.m, 0.0f, 1.0f); + ImGui::SliderFloat("Linear section length", &pcHDR.l, 0.0f, 1.0f); + ImGui::SliderFloat("Black tightness", &pcHDR.c, 1.0f, 3.0f); + ImGui::SliderFloat("Pedestal", &pcHDR.b, 0.0f, 1.0f); + ImGui::EndDisabled(); + ImGui::Unindent(indentSize); + } + ImGui::RadioButton("Khronos PBR Neutral", &pcHDR.drawMode, ToneMapping_KhronosPBR); + if (pcHDR.drawMode == ToneMapping_KhronosPBR) { + ImGui::Indent(indentSize); + ImGui::SliderFloat("Highlight compression start", &pcHDR.startCompression, 0.0f, 1.0f); + ImGui::SliderFloat("Desaturation speed", &pcHDR.desaturation, 0.0f, 1.0f); + ImGui::Unindent(indentSize); + } + ImGui::Separator(); + + ImGui::Text("Average luminance 1x1:"); + ImGui::Image(texLuminanceViews[LVK_ARRAY_NUM_ELEMENTS(texLuminanceViews) - 1].index(), ImVec2(128, 128)); + ImGui::Text("Adapted luminance 1x1:"); + ImGui::Image(texAdaptedLuminance[0].index(), ImVec2(128, 128)); + ImGui::Separator(); + ImGui::Text("Bright pass:"); + ImGui::Image(texBrightPass.index(), ImVec2(windowWidth, windowWidth / aspectRatio)); + ImGui::Text("Bloom pass:"); + ImGui::Image(texBloomPass.index(), ImVec2(windowWidth, windowWidth / aspectRatio)); + ImGui::Separator(); + ImGui::Text("Luminance pyramid 512x512"); + for (uint32_t l = 0; l != LVK_ARRAY_NUM_ELEMENTS(texLuminanceViews); l++) { + ImGui::Image(texLuminanceViews[l].index(), ImVec2((int)windowWidth >> l, ((int)windowWidth >> l))); + } + ImGui::Separator(); + ImGui::End(); + + if (drawCurves) { + const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; + ImGui::SetNextWindowBgAlpha(0.8f); + ImGui::SetNextWindowPos({ width * 0.6f, height * 0.7f }, ImGuiCond_Appearing); + ImGui::SetNextWindowSize({ width * 0.4f, height * 0.3f }); + ImGui::Begin("Tone mapping curve", nullptr, flags); + const int kNumGraphPoints = 1001; + float xs[kNumGraphPoints]; + float ysUnchimura[kNumGraphPoints]; + float ysReinhard2[kNumGraphPoints]; + float ysKhronosPBR[kNumGraphPoints]; + for (int i = 0; i != kNumGraphPoints; i++) { + xs[i] = float(i) / kNumGraphPoints; + ysUnchimura[i] = uchimura(xs[i], pcHDR.P, pcHDR.a, pcHDR.m, pcHDR.l, pcHDR.c, pcHDR.b); + ysReinhard2[i] = reinhard2(xs[i], pcHDR.maxWhite); + ysKhronosPBR[i] = PBRNeutralToneMapping(xs[i], pcHDR.startCompression, pcHDR.desaturation); + } + if (ImPlot::BeginPlot("Tone mapping curves", { width * 0.4f, height * 0.3f }, ImPlotFlags_NoInputs)) { + ImPlot::SetupAxes("Input", "Output"); + ImPlot::PlotLine("Uchimura", xs, ysUnchimura, kNumGraphPoints); + ImPlot::PlotLine("Reinhard", xs, ysReinhard2, kNumGraphPoints); + ImPlot::PlotLine("Khronos PBR", xs, ysKhronosPBR, kNumGraphPoints); + ImPlot::EndPlot(); + } + ImGui::End(); + } + } + + app.imgui_->endFrame(buf); + + buf.cmdEndRendering(); + } + ctx->submit(buf, ctx->getCurrentSwapchainTexture()); + + // swap ping-bong textures + std::swap(texAdaptedLuminance[0], texAdaptedLuminance[1]); + }); + + ImPlot::DestroyContext(implotCtx); + + ctx.release(); + + return 0; +} diff --git a/Chapter10/Bistro.h b/Chapter10/Bistro.h new file mode 100644 index 0000000..c651a9e --- /dev/null +++ b/Chapter10/Bistro.h @@ -0,0 +1,78 @@ +#pragma once + +#include "shared/Scene/MergeUtil.h" +#include "shared/Scene/Scene.h" +#include "shared/Scene/VtxData.h" + +#include "Chapter08/SceneUtils.h" +#include "Chapter08/VKMesh08.h" + +const char* fileNameCachedMeshes = ".cache/ch08_bistro.meshes"; +const char* fileNameCachedMaterials = ".cache/ch08_bistro.materials"; +const char* fileNameCachedHierarchy = ".cache/ch08_bistro.scene"; + +void loadBistro(MeshData& meshData, Scene& scene) { + if (!isMeshDataValid(fileNameCachedMeshes) || !isMeshHierarchyValid(fileNameCachedHierarchy) || + !isMeshMaterialsValid(fileNameCachedMaterials)) { + printf("No cached mesh data found. Precaching...\n\n"); + + MeshData meshData_Exterior; + MeshData meshData_Interior; + Scene ourScene_Exterior; + Scene ourScene_Interior; + + // don't generate LODs because meshoptimizer fails on the Bistro mesh + loadMeshFile("deps/src/bistro/Exterior/exterior.obj", meshData_Exterior, ourScene_Exterior, false); + loadMeshFile("deps/src/bistro/Interior/interior.obj", meshData_Interior, ourScene_Interior, false); + + // merge some meshes + printf("[Unmerged] scene items: %u\n", (uint32_t)ourScene_Exterior.hierarchy.size()); + mergeNodesWithMaterial(ourScene_Exterior, meshData_Exterior, "Foliage_Linde_Tree_Large_Orange_Leaves"); + printf("[Merged orange leaves] scene items: %u\n", (uint32_t)ourScene_Exterior.hierarchy.size()); + mergeNodesWithMaterial(ourScene_Exterior, meshData_Exterior, "Foliage_Linde_Tree_Large_Green_Leaves"); + printf("[Merged green leaves] scene items: %u\n", (uint32_t)ourScene_Exterior.hierarchy.size()); + mergeNodesWithMaterial(ourScene_Exterior, meshData_Exterior, "Foliage_Linde_Tree_Large_Trunk"); + printf("[Merged trunk] scene items: %u\n", (uint32_t)ourScene_Exterior.hierarchy.size()); + + // merge everything into one big scene + MeshData meshData; + Scene ourScene; + + mergeScenes( + ourScene, + { + &ourScene_Exterior, + &ourScene_Interior, + }, + {}, + { + static_cast<uint32_t>(meshData_Exterior.meshes.size()), + static_cast<uint32_t>(meshData_Interior.meshes.size()), + }); + mergeMeshData(meshData, { &meshData_Exterior, &meshData_Interior }); + mergeMaterialLists( + { + &meshData_Exterior.materials, + &meshData_Interior.materials, + }, + { + &meshData_Exterior.textureFiles, + &meshData_Interior.textureFiles, + }, + meshData.materials, meshData.textureFiles); + + ourScene.localTransform[0] = glm::scale(vec3(0.01f)); // scale the Bistro + markAsChanged(ourScene, 0); + + recalculateBoundingBoxes(meshData); + + saveMeshData(fileNameCachedMeshes, meshData); + saveMeshDataMaterials(fileNameCachedMaterials, meshData); + saveScene(fileNameCachedHierarchy, ourScene); + } + + const MeshFileHeader header = loadMeshData(fileNameCachedMeshes, meshData); + loadMeshDataMaterials(fileNameCachedMaterials, meshData); + + loadScene(fileNameCachedHierarchy, scene); +} diff --git a/Chapter10/Skybox.h b/Chapter10/Skybox.h new file mode 100644 index 0000000..eacd7f3 --- /dev/null +++ b/Chapter10/Skybox.h @@ -0,0 +1,48 @@ +#pragma once + +class Skybox +{ +public: + Skybox( + const std::unique_ptr<lvk::IContext>& ctx, const char* skyboxTexture, const char* skyboxIrradiance, lvk::Format colorFormat, + lvk::Format depthFormat, uint32_t numSamples = 1) + { + texSkybox = loadTexture(ctx, skyboxTexture, lvk::TextureType_Cube); + texSkyboxIrradiance = loadTexture(ctx, skyboxIrradiance, lvk::TextureType_Cube); + + vertSkybox = loadShaderModule(ctx, "Chapter08/02_SceneGraph/src/skybox.vert"); + fragSkybox = loadShaderModule(ctx, "Chapter08/02_SceneGraph/src/skybox.frag"); + pipelineSkybox = ctx->createRenderPipeline({ + .smVert = vertSkybox, + .smFrag = fragSkybox, + .color = { { .format = colorFormat } }, + .depthFormat = depthFormat, + .samplesCount = numSamples, + }); + } + + void draw(lvk::ICommandBuffer& buf, const mat4& view, const mat4& proj) const + { + buf.cmdPushDebugGroupLabel("Skybox", 0xff0000ff); + buf.cmdBindRenderPipeline(pipelineSkybox); + const struct { + mat4 view; + mat4 proj; + uint32_t texSkybox; + } pc = { + .view = view, + .proj = proj, + .texSkybox = texSkybox.index(), + }; + buf.cmdPushConstants(pc); + buf.cmdBindDepthState({ .isDepthWriteEnabled = false }); + buf.cmdDraw(36); + buf.cmdPopDebugGroupLabel(); + } + + lvk::Holder<lvk::TextureHandle> texSkybox; + lvk::Holder<lvk::TextureHandle> texSkyboxIrradiance; + lvk::Holder<lvk::ShaderModuleHandle> vertSkybox; + lvk::Holder<lvk::ShaderModuleHandle> fragSkybox; + lvk::Holder<lvk::RenderPipelineHandle> pipelineSkybox; +}; diff --git a/deps/bootstrap.json b/deps/bootstrap.json index aa80fde..3306bf4 100644 --- a/deps/bootstrap.json +++ b/deps/bootstrap.json @@ -4,7 +4,7 @@ "source": { "type": "git", "url": "https://github.com/corporateshark/lightweightvk.git", - "revision": "85f753c7b7ff92b3f45704446632436db7a6191b" + "revision": "d653552f0f7e6f1fd19752e4f2f9258d6151408b" } }, {