diff --git a/CMakeLists.txt b/CMakeLists.txt index 90c54cfa69095fc41300065ab26ab22d7215eef0..d68b269f36170faf92d5ba6d89b0eac9d060bbca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -565,6 +565,28 @@ message("======================================================================= target_link_libraries(lava-spawn lava::demo) set_property(TARGET lava-spawn PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${PROJECT_BINARY_DIR}") + + message("> lava-light") + + set(LIGHT_SHADERS + res/light/gbuffer.frag + res/light/gbuffer.vert + res/light/lighting.frag + res/light/lighting.vert + res/light/data.inc + ) + + add_executable(lava-light + ${LIBLAVA_DEMO_DIR}/light.cpp + ${LIGHT_SHADERS} + ) + + source_group("Shader Files" FILES ${LIGHT_SHADERS}) + + set_target_properties(lava-light PROPERTIES FOLDER "lava-demo") + target_link_libraries(lava-light lava::demo) + + set_property(TARGET lava-light PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${PROJECT_BINARY_DIR}") endif() option(LIBLAVA_TEMPLATE "Enable Template" TRUE) diff --git a/liblava-demo/light.cpp b/liblava-demo/light.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a1d4f27c2d6838d2b7569cb5853618adefe82217 --- /dev/null +++ b/liblava-demo/light.cpp @@ -0,0 +1,460 @@ +// file : liblava-demo/light.cpp +// copyright : Copyright (c) 2018-present, Lava Block OÜ and contributors +// license : MIT; see accompanying LICENSE file + +#include <imgui.h> +#include <demo.hpp> + +using namespace lava; + +// structs for interfacing with shaders +namespace glsl +{ + using namespace glm; + using uint = uint32_t; +#include "res/light/data.inc" +} + +glsl::UboData g_ubo; + +struct gbuffer_attachment { + enum type : uint32_t { + albedo = 0, + normal, + metallic_roughness, + depth, + count + }; + + VkFormats requested_formats; + VkImageUsageFlags usage; + image::ptr image_handle; + attachment::ptr renderpass_attachment; + VkAttachmentReference subpass_reference; + + bool create(uint32_t index); +}; + +using attachment_array = std::array<gbuffer_attachment, gbuffer_attachment::count>; +attachment_array g_attachments = { + gbuffer_attachment{ { VK_FORMAT_R8G8B8A8_UNORM }, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT }, + gbuffer_attachment{ { VK_FORMAT_R16G16B16A16_SFLOAT }, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT }, + gbuffer_attachment{ { VK_FORMAT_R16G16_SFLOAT }, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT }, + gbuffer_attachment{ { VK_FORMAT_D32_SFLOAT, VK_FORMAT_D16_UNORM }, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT }, +}; + +using light_array = std::array<glsl::LightData, 3>; +const light_array g_lights = { + glsl::LightData{ { 2.0f, 2.0f, 2.5f }, 10.0f, { 30.0f, 10.0f, 10.0f } }, + glsl::LightData{ { -2.0f, -2.0f, -0.5f }, 10.0f, { 10.0f, 30.0f, 10.0f } }, + glsl::LightData{ { 0.0f, 0.0f, -1.5f }, 10.0f, { 10.0f, 10.0f, 30.0f } } +}; + +app* g_app = nullptr; + +render_pass::ptr create_gbuffer_renderpass(attachment_array& attachments); + +int main(int argc, char* argv[]) { + app app("lava light", { argc, argv }); + if (!app.setup()) + return error::not_ready; + + target_callback resize_callback; + app.target->add_callback(&resize_callback); + + g_app = &app; + + // create global immutable resources + // destroyed in app.add_run_end + + mesh::ptr object = create_mesh(app.device, mesh_type::quad); + if (!object) + return error::create_failed; + + using object_array = std::array<mat4, 2>; + object_array object_instances; + + texture::ptr tex_normal = load_texture(app.device, "light/normal.png"); + texture::ptr tex_roughness = load_texture(app.device, "light/roughness.png"); + if (!tex_normal || !tex_roughness) + return error::create_failed; + + app.staging.add(tex_normal); + app.staging.add(tex_roughness); + + buffer ubo_buffer; + if (!ubo_buffer.create_mapped(app.device, nullptr, sizeof(g_ubo), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT)) + return error::create_failed; + + buffer light_buffer; + if (!light_buffer.create_mapped(app.device, g_lights.data(), sizeof(g_lights), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT)) + return error::create_failed; + + const VkSamplerCreateInfo sampler_info = { + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .magFilter = VK_FILTER_NEAREST, + .minFilter = VK_FILTER_NEAREST, + .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST + }; + VkSampler sampler; + if (!app.device->vkCreateSampler(&sampler_info, &sampler)) + return error::create_failed; + + // pipeline-specific resources + // created in app.on_create, destroyed in app.on_destroy + + descriptor::pool descriptor_pool; + + render_pass::ptr gbuffer_renderpass = make_render_pass(app.device); + descriptor::ptr gbuffer_set_layout = make_descriptor(); + pipeline_layout::ptr gbuffer_pipeline_layout = make_pipeline_layout(); + graphics_pipeline::ptr gbuffer_pipeline = make_graphics_pipeline(app.device); + VkDescriptorSet gbuffer_set = VK_NULL_HANDLE; + + descriptor::ptr lighting_set_layout = make_descriptor(); + pipeline_layout::ptr lighting_pipeline_layout = make_pipeline_layout(); + graphics_pipeline::ptr lighting_pipeline = make_graphics_pipeline(app.device); + VkDescriptorSet lighting_set = VK_NULL_HANDLE; + + app.on_create = [&]() { + const VkDescriptorPoolSizes pool_sizes = { + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1 * 2 }, // one uniform buffer for each pass (gbuffer + lighting) + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1 }, // light buffer + { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2 /* normal + roughness texture */ + g_attachments.size() }, + }; + constexpr ui32 max_sets = 2; // one for each pass + if (!descriptor_pool.create(app.device, pool_sizes, max_sets)) + return false; + + // gbuffer pass + + gbuffer_set_layout->add_binding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT); + gbuffer_set_layout->add_binding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT); + gbuffer_set_layout->add_binding(2, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT); + if (!gbuffer_set_layout->create(app.device)) + return false; + gbuffer_set = gbuffer_set_layout->allocate(descriptor_pool.get()); + if (!gbuffer_set) + return false; + + std::vector<VkWriteDescriptorSet> gbuffer_write_sets; + for (const descriptor::binding::ptr& binding : gbuffer_set_layout->get_bindings()) { + const VkDescriptorSetLayoutBinding& info = binding->get(); + gbuffer_write_sets.push_back({ .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = gbuffer_set, + .dstBinding = info.binding, + .descriptorCount = info.descriptorCount, + .descriptorType = info.descriptorType }); + } + + gbuffer_write_sets[0].pBufferInfo = ubo_buffer.get_descriptor_info(); + gbuffer_write_sets[1].pImageInfo = tex_normal->get_descriptor_info(); + gbuffer_write_sets[2].pImageInfo = tex_roughness->get_descriptor_info(); + + app.device->vkUpdateDescriptorSets(gbuffer_write_sets.size(), gbuffer_write_sets.data()); + + gbuffer_pipeline_layout->add(gbuffer_set_layout); + gbuffer_pipeline_layout->add_range({ VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(glsl::PushConstantData) }); + if (!gbuffer_pipeline_layout->create(app.device)) + return false; + + const VkPipelineColorBlendAttachmentState gbuffer_blend_state = { + .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT + }; + + if (!gbuffer_pipeline->add_shader(file_data("light/gbuffer.vertex.spirv"), VK_SHADER_STAGE_VERTEX_BIT)) + return false; + if (!gbuffer_pipeline->add_shader(file_data("light/gbuffer.fragment.spirv"), VK_SHADER_STAGE_FRAGMENT_BIT)) + return false; + for (size_t i = 0; i < g_attachments.size() - 1; i++) { + gbuffer_pipeline->add_color_blend_attachment(gbuffer_blend_state); + } + gbuffer_pipeline->set_depth_test_and_write(true, true); + gbuffer_pipeline->set_depth_compare_op(VK_COMPARE_OP_LESS); + gbuffer_pipeline->set_rasterization_cull_mode(VK_CULL_MODE_NONE); + gbuffer_pipeline->set_vertex_input_binding({ 0, sizeof(vertex), VK_VERTEX_INPUT_RATE_VERTEX }); + gbuffer_pipeline->set_vertex_input_attributes({ + { 0, 0, VK_FORMAT_R32G32B32_SFLOAT, to_ui32(offsetof(vertex, position)) }, + { 1, 0, VK_FORMAT_R32G32_SFLOAT, to_ui32(offsetof(vertex, uv)) }, + { 2, 0, VK_FORMAT_R32G32B32_SFLOAT, to_ui32(offsetof(vertex, normal)) }, + }); + gbuffer_pipeline->set_layout(gbuffer_pipeline_layout); + gbuffer_pipeline->set_auto_size(true); + + gbuffer_pipeline->on_process = [&](VkCommandBuffer cmd_buf) { + scoped_label label(cmd_buf, "gbuffer"); + + gbuffer_pipeline_layout->bind(cmd_buf, gbuffer_set); + object->bind(cmd_buf); + + for (size_t i = 0; i < object_instances.size(); i++) { + const glsl::PushConstantData pc = { + .model = object_instances[i], + .color = v3(1.0f), + .metallic = float(i % 2), + .enableNormalMapping = 1 - (i % 2) + }; + app.device->call().vkCmdPushConstants(cmd_buf, gbuffer_pipeline_layout->get(), VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof(pc), &pc); + object->draw(cmd_buf); + } + }; + + gbuffer_renderpass = create_gbuffer_renderpass(g_attachments); + gbuffer_renderpass->add_front(gbuffer_pipeline); + + // lighting pass + + for (size_t i = 0; i < g_attachments.size(); i++) { + lighting_set_layout->add_binding(i, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT); + } + lighting_set_layout->add_binding(g_attachments.size() + 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_FRAGMENT_BIT); + lighting_set_layout->add_binding(g_attachments.size() + 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_FRAGMENT_BIT); + if (!lighting_set_layout->create(app.device)) + return false; + lighting_set = lighting_set_layout->allocate(descriptor_pool.get()); + if (!lighting_set) + return false; + + lighting_pipeline_layout->add(lighting_set_layout); + if (!lighting_pipeline_layout->create(app.device)) + return false; + + const VkPipelineColorBlendAttachmentState lighting_blend_state = { + .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT + }; + + if (!lighting_pipeline->add_shader(file_data("light/lighting.vertex.spirv"), VK_SHADER_STAGE_VERTEX_BIT)) + return false; + if (!lighting_pipeline->add_shader(file_data("light/lighting.fragment.spirv"), VK_SHADER_STAGE_FRAGMENT_BIT)) + return false; + lighting_pipeline->add_color_blend_attachment(lighting_blend_state); + lighting_pipeline->set_rasterization_cull_mode(VK_CULL_MODE_NONE); + lighting_pipeline->set_layout(lighting_pipeline_layout); + lighting_pipeline->set_auto_size(true); + + lighting_pipeline->on_process = [&](VkCommandBuffer cmd_buf) { + scoped_label label(cmd_buf, "lighting"); + + // run a fullscreen pass to calculate lighting, the shader loops over all lights + // - this is NOT very performant, but simplifies the demo + // - in a proper deferred renderer you most likely want to: + // - render light geometries (e.g. spheres) while depth testing against the gbuffer depth + // - use some kind of spatial acceleration structure for lights + + lighting_pipeline_layout->bind(cmd_buf, lighting_set); + app.device->call().vkCmdDraw(cmd_buf, 3, 1, 0, 0); + }; + + // use lava's default backbuffer renderpass + render_pass::ptr lighting_renderpass = app.shading.get_pass(); + lighting_renderpass->add_front(lighting_pipeline); + + // the resize callback creates the gbuffer images and renderpass, call it once manually + if (!resize_callback.on_created({}, { { 0, 0 }, app.target->get_size() })) + return false; + + // renderpasses have been created at this point, actually create the pipelines + if (!gbuffer_pipeline->create(gbuffer_renderpass->get())) + return false; + if (!lighting_pipeline->create(lighting_renderpass->get())) + return false; + + return true; + }; + + app.on_process = [&](VkCommandBuffer cmd_buf, index frame) { + scoped_label label(cmd_buf, "on_process"); + + // start custom renderpass, run on_process() for each pipeline added to the renderpass + gbuffer_renderpass->process(cmd_buf); + }; + + app.on_update = [&](delta dt) { + float seconds = to_delta(app.get_running_time()); + constexpr float distance = 1.25f; + const float left = -distance * (object_instances.size() - 1) * 0.5f; + for (size_t i = 0; i < object_instances.size(); i++) { + float x = left + distance * i; + v3 axis = v3(0.0f); + axis[i % 3] = 1.0f; + mat4 model = mat4(1.0f); + model = glm::translate(model, { x, 0.0f, 0.0f }); + model = glm::rotate(model, glm::radians(std::fmod(seconds * 45.0f, 360.0f)), axis); + model = glm::scale(model, { 0.5f, 0.5f, 0.5f }); + object_instances[i] = model; + } + + return true; + }; + + // handle backbuffer resize + + resize_callback.on_created = [&](VkAttachmentsRef, rect area) { + // update uniform buffer + g_ubo.camPos = { 0.0f, 0.0f, -1.25f }; + g_ubo.lightCount = g_lights.size(); + g_ubo.view = glm::lookAtLH(g_ubo.camPos, { 0.0f, 0.0f, 0.0f }, { 0.0f, 1.0f, 0.0f }); + g_ubo.projection = perspective_matrix(area.get_size(), 90.0f, 3.0f); + g_ubo.invProjection = glm::inverse(g_ubo.projection); + g_ubo.resolution = area.get_size(); + *(decltype(g_ubo)*) ubo_buffer.get_mapped_data() = g_ubo; + + // (re-)create gbuffer attachments and collect views for framebuffer creation + VkImageViews views; + for (gbuffer_attachment& att : g_attachments) { + if (!att.image_handle->create(app.device, area.get_size())) + return false; + views.push_back(att.image_handle->get_view()); + } + + // update lighting descriptor set with new gbuffer image handles + std::vector<VkWriteDescriptorSet> lighting_write_sets; + for (const descriptor::binding::ptr& binding : lighting_set_layout->get_bindings()) { + const VkDescriptorSetLayoutBinding& info = binding->get(); + lighting_write_sets.push_back({ .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = lighting_set, + .dstBinding = info.binding, + .descriptorCount = info.descriptorCount, + .descriptorType = info.descriptorType }); + } + + std::array<VkDescriptorImageInfo, g_attachments.size()> lighting_images; + for (size_t i = 0; i < g_attachments.size(); i++) { + lighting_images[i] = { + .sampler = sampler, + .imageView = g_attachments[i].image_handle->get_view(), + .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + }; + lighting_write_sets[i].pImageInfo = &lighting_images[i]; + } + lighting_write_sets[g_attachments.size() + 0].pBufferInfo = ubo_buffer.get_descriptor_info(); + lighting_write_sets[g_attachments.size() + 1].pBufferInfo = light_buffer.get_descriptor_info(); + + app.device->vkUpdateDescriptorSets(lighting_write_sets.size(), lighting_write_sets.data()); + + // create framebuffer (and renderpass if necessary) + if (gbuffer_renderpass->get() == VK_NULL_HANDLE) + return gbuffer_renderpass->create({ views }, area); + else + return gbuffer_renderpass->on_created({ views }, area); + }; + + resize_callback.on_destroyed = [&]() { + app.device->wait_for_idle(); + // destroy framebuffer + gbuffer_renderpass->on_destroyed(); + // destroy gbuffer attachments + for (gbuffer_attachment& att : g_attachments) { + att.image_handle->destroy(); + } + }; + + app.imgui.on_draw = [&]() { + ImGui::SetNextWindowPos(ImVec2(30, 30), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(262, 262), ImGuiCond_FirstUseEver); + + ImGui::Begin(app.get_name()); + + app.draw_about(); + + ImGui::End(); + }; + + app.on_destroy = [&]() { + app.target->remove_callback(&resize_callback); + resize_callback.on_destroyed(); + + lighting_pipeline->destroy(); + lighting_pipeline_layout->destroy(); + lighting_set_layout->destroy(); + + gbuffer_pipeline->destroy(); + gbuffer_pipeline_layout->destroy(); + gbuffer_set_layout->destroy(); + gbuffer_renderpass->destroy(); + + descriptor_pool.destroy(); + }; + + app.add_run_end([&]() { + app.device->vkDestroySampler(sampler); + sampler = VK_NULL_HANDLE; + + light_buffer.destroy(); + ubo_buffer.destroy(); + + tex_roughness->destroy(); + tex_normal->destroy(); + + object->destroy(); + }); + + return app.run(); +} + +bool gbuffer_attachment::create(uint32_t index) { + usage |= VK_IMAGE_USAGE_SAMPLED_BIT; + std::optional<VkFormat> format = get_supported_format(g_app->device->get_vk_physical_device(), requested_formats, usage); + if (!format.has_value()) + return false; + + image_handle = make_image(*format); + image_handle->set_usage(usage); + + renderpass_attachment = make_attachment(*format); + renderpass_attachment->set_op(VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_STORE); + renderpass_attachment->set_stencil_op(VK_ATTACHMENT_LOAD_OP_DONT_CARE, VK_ATTACHMENT_STORE_OP_DONT_CARE); + renderpass_attachment->set_layouts(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + + subpass_reference.attachment = index; + subpass_reference.layout = (usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) + ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL + : VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + + return true; +} + +render_pass::ptr create_gbuffer_renderpass(attachment_array& attachments) { + VkClearValues clear_values(attachments.size(), { .color = { 0.0f, 0.0f, 0.0f, 1.0f } }); + clear_values[gbuffer_attachment::depth] = { .depthStencil = { 1.0f, 0 } }; + + render_pass::ptr pass = make_render_pass(g_app->device); + pass->set_clear_values(clear_values); + + VkAttachmentReferences color_attachments; + for (uint32_t i = 0; i < gbuffer_attachment::count; i++) { + if (!attachments[i].create(i)) + return nullptr; + pass->add(attachments[i].renderpass_attachment); + if (i != gbuffer_attachment::depth) + color_attachments.push_back(attachments[i].subpass_reference); + } + + subpass::ptr sub = make_subpass(); + sub->set_color_attachments(color_attachments); + sub->set_depth_stencil_attachment(attachments[gbuffer_attachment::depth].subpass_reference); + pass->add(sub); + + subpass_dependency::ptr dependency = make_subpass_dependency(VK_SUBPASS_EXTERNAL, 0); + // wait for previous fragment shader to finish reading before clearing attachments + dependency->set_stage_mask( + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT); + // we need a memory barrier because this isn't a standard write-after-read hazard + // subpass deps have an implicit attachment layout transition, so the dst access mask must be correct + dependency->set_access_mask(0, + VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + pass->add(dependency); + + dependency = make_subpass_dependency(pass->get_subpass_count() - 1, VK_SUBPASS_EXTERNAL); + // don't run any fragment shader (sample attachments) before we're done writing to attachments + dependency->set_stage_mask(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + // make attachment writes visible to subsequent reads + dependency->set_access_mask(VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, + VK_ACCESS_SHADER_READ_BIT); + pass->add(dependency); + + return pass; +} diff --git a/res/light/data.inc b/res/light/data.inc new file mode 100644 index 0000000000000000000000000000000000000000..a61b8bbb65b9e057ceba48812180f3f40ee46976 --- /dev/null +++ b/res/light/data.inc @@ -0,0 +1,25 @@ +struct UboData +{ + vec3 camPos; + uint lightCount; + mat4 view; + mat4 projection; + mat4 invProjection; + uvec2 resolution; +}; + +struct PushConstantData +{ + mat4 model; + vec3 color; + float metallic; + uint enableNormalMapping; +}; + +struct LightData +{ + vec3 position; + float radius; + vec3 intensity; + float _padding; +}; diff --git a/res/light/gbuffer.frag b/res/light/gbuffer.frag new file mode 100644 index 0000000000000000000000000000000000000000..17904f116e9dbc913f7660a6b7d5ada43712ca45 --- /dev/null +++ b/res/light/gbuffer.frag @@ -0,0 +1,57 @@ +#version 450 core +#extension GL_GOOGLE_include_directive : require + +#include "data.inc" + +layout (location = 0) in vec3 inPos; +layout (location = 1) in vec2 inUV; +layout (location = 2) in vec3 inNormal; + +layout(push_constant) uniform PushConstants +{ + PushConstantData pc; +}; + +layout (binding = 1) uniform sampler2D samplerNormal; +layout (binding = 2) uniform sampler2D samplerRoughness; + +layout (location = 0) out vec4 outAlbedo; +layout (location = 1) out vec4 outNormal; +layout (location = 2) out vec2 outMetallicRoughness; + +// Schlüter 2013. Normal Mapping Without Precomputed Tangents. +// http://www.thetenthplanet.de/archives/1180 +mat3 cotangentFrame(vec3 N, vec3 pos, vec2 uv) +{ + vec3 dPx = dFdx(pos); + vec3 dPy = dFdy(pos); + vec2 dTx = dFdx(uv); + vec2 dTy = dFdy(uv); + + vec3 dPxC = cross(N, dPx); + vec3 dPyC = cross(dPy, N); + + vec3 T = dPyC * dTx.x + dPxC * dTy.x; + vec3 B = dPyC * dTx.y + dPxC * dTy.y; + + float invmax = inversesqrt(max(dot(T, T), dot(B, B))); + + return mat3(T * invmax, B * invmax, N); +} + +void main() +{ + vec3 normal = normalize(inNormal); + if(pc.enableNormalMapping != 0) + { + mat3 TBN = cotangentFrame(normal, inPos, inUV); + vec3 tangentNormal = texture(samplerNormal, inUV).xyz * 2.0 - 1.0; + normal = normalize(TBN * tangentNormal); + } + + float roughness = texture(samplerRoughness, inUV).x; + + outAlbedo = vec4(pc.color, 1.0); + outNormal = vec4(normal, 1.0); + outMetallicRoughness = vec2(pc.metallic, roughness); +} diff --git a/res/light/gbuffer.fragment.spirv b/res/light/gbuffer.fragment.spirv new file mode 100644 index 0000000000000000000000000000000000000000..16a9aef8b901eb4f7f995163e90cef61a4b76bdd Binary files /dev/null and b/res/light/gbuffer.fragment.spirv differ diff --git a/res/light/gbuffer.vert b/res/light/gbuffer.vert new file mode 100644 index 0000000000000000000000000000000000000000..c1a53258366d80e35fff16bf6805d922b6535974 --- /dev/null +++ b/res/light/gbuffer.vert @@ -0,0 +1,35 @@ +#version 450 core +#extension GL_GOOGLE_include_directive : require + +#include "data.inc" + +layout (location = 0) in vec3 inPos; +layout (location = 1) in vec2 inUV; +layout (location = 2) in vec3 inNormal; + +layout(push_constant) uniform PushConstants +{ + PushConstantData pc; +}; + +layout (binding = 0) uniform Ubo +{ + UboData ubo; +}; + +layout (location = 0) out vec3 outPos; +layout (location = 1) out vec2 outUV; +layout (location = 2) out vec3 outNormal; + +void main() +{ + outPos = vec3(pc.model * vec4(inPos, 1.0)); + outUV = inUV; + outNormal = normalize(mat3(pc.model) * inNormal); + // correctly render double-sided materials + vec3 V = normalize(ubo.camPos - outPos); + if(dot(outNormal, V) < 0.0) + outNormal = -outNormal; + + gl_Position = ubo.projection * ubo.view * vec4(outPos, 1.0); +} diff --git a/res/light/gbuffer.vertex.spirv b/res/light/gbuffer.vertex.spirv new file mode 100644 index 0000000000000000000000000000000000000000..f218de66b47c0da2feb228b38a491c9cc023a5c2 Binary files /dev/null and b/res/light/gbuffer.vertex.spirv differ diff --git a/res/light/gen_spirv.bat b/res/light/gen_spirv.bat new file mode 100644 index 0000000000000000000000000000000000000000..dedef7221ba685ff03eb039a5429ebefff63a3ba --- /dev/null +++ b/res/light/gen_spirv.bat @@ -0,0 +1,7 @@ +@ECHO on + +glslangValidator -V -o gbuffer.fragment.spirv gbuffer.frag +glslangValidator -V -o gbuffer.vertex.spirv gbuffer.vert + +glslangValidator -V -o lighting.fragment.spirv lighting.frag +glslangValidator -V -o lighting.vertex.spirv lighting.vert diff --git a/res/light/gen_spirv.sh b/res/light/gen_spirv.sh new file mode 100644 index 0000000000000000000000000000000000000000..3c4b2c65deb92de6f79e9b3b210f7f63d5f77305 --- /dev/null +++ b/res/light/gen_spirv.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +glslangValidator -V -o gbuffer.fragment.spirv gbuffer.frag +glslangValidator -V -o gbuffer.vertex.spirv gbuffer.vert + +glslangValidator -V -o lighting.fragment.spirv lighting.frag +glslangValidator -V -o lighting.vertex.spirv lighting.vert diff --git a/res/light/lighting.frag b/res/light/lighting.frag new file mode 100644 index 0000000000000000000000000000000000000000..a6e800d074d8206d3cacd98e9994e561cd8e2a3c --- /dev/null +++ b/res/light/lighting.frag @@ -0,0 +1,171 @@ +#version 450 core +#extension GL_GOOGLE_include_directive : require + +#include "data.inc" + +layout (binding = 0) uniform sampler2D samplerGbufferAlbedo; +layout (binding = 1) uniform sampler2D samplerGbufferNormal; +layout (binding = 2) uniform sampler2D samplerGbufferMetallicRoughness; +layout (binding = 3) uniform sampler2D samplerGbufferDepth; + +layout (binding = 4) uniform Ubo +{ + UboData ubo; +}; + +layout (binding = 5) restrict readonly buffer Sbo_Lights +{ + LightData lights[]; +}; + +layout(location = 0) out vec4 outFragColor; + +// Physically based shading, metallic + roughness workflow (GLTF 2.0 core material spec) +// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material +// Some GLSL code adapted from +// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#appendix-b-brdf-implementation +// https://google.github.io/filament/Filament.md.html + +#define INV_PI 0.31831 + +struct Material +{ + vec3 diffuse; + vec3 f0; + float a; +}; + +// Schlick approximation to Fresnel equation using F90 = 1 +// Schlick 1994. An Inexpensive BRDF Model for Physically based Rendering. +vec3 F_Schlick(float VoH, vec3 F0) +{ + float f = pow(1.0 - VoH, 5.0); + return f + F0 * (1.0 - f); +} + +// GGX Normal Distribution Function +// Bruce Walter et al. 2007. Microfacet Models for Refraction through Rough Surfaces. +float D_GGX(float NoH, float a) +{ + a = NoH * a; + float k = a / (1.0 - NoH * NoH + a * a); + return k * k * INV_PI; +} + +// GGX Geometric Shadowing/Occlusion Function, based on Smith approach +// height-correlated joint masking-shadowing function +// Heitz 2014. Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs. +float V_SmithGGXCorrelated(float NoV, float NoL, float a) +{ + float a2 = a * a; + float GGXV = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2); + float GGXL = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2); + return 0.5 / (GGXV + GGXL); +} + +// Lambertian diffuse BRDF +vec3 diffuseBrdf(Material mat, float VoH) +{ + vec3 F = F_Schlick(VoH, mat.f0); + return (1.0 - F) * mat.diffuse * INV_PI; +} + +vec3 specularBrdf(Material mat, float NoH, float VoH, float NoV, float NoL) +{ + float D = D_GGX(NoH, mat.a); + vec3 F = F_Schlick(VoH, mat.f0); + float V = V_SmithGGXCorrelated(NoV, NoL, mat.a); + + return (D * V) * F; +} + +// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#appendix-b-brdf-implementation +vec3 brdf(Material mat, vec3 v, vec3 l, vec3 n) +{ + vec3 h = normalize(l + v); + + float NoV = abs(dot(n, v)) + 1e-5; + float NoL = clamp(dot(n, l), 0.0, 1.0); + float NoH = clamp(dot(n, h), 0.0, 1.0); + float VoH = clamp(dot(v, h), 0.0, 1.0); + + return specularBrdf(mat, NoH, VoH, NoV, NoL) + diffuseBrdf(mat, VoH); +} + +// inverse square falloff +float distanceAttenuation(float distance) +{ + return 1.0 / max(distance * distance, 0.01 * 0.01); +} + +// windowing function with smooth transition to 0 +// radius is arbitrary +// Karis 2014. Real Shading in Unreal Engine 4. +float distanceAttenuation(float distance, float radius) +{ + float nom = clamp(1.0 - pow(distance / radius, 4.0), 0.0, 1.0); + return nom * nom * distanceAttenuation(distance); +} + +vec3 screenToEye(vec3 fragCoord, uvec2 resolution, mat4 invProjection) +{ + vec3 ndc = vec3( + 2.0 * fragCoord.xy / vec2(resolution) - 1.0, // -> [-1;1] + fragCoord.z // [0;1] + ); + vec4 eye = invProjection * vec4(ndc, 1.0); + return eye.xyz / eye.w; +} + +// luminance-only fit of ACES LDR output transform +// Narkowicz 2016. ACES Filmic Tone Mapping Curve. +// https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/ +vec3 tonemap(vec3 x) +{ + return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0); +} + +void main() +{ + vec2 uv = gl_FragCoord.xy / vec2(ubo.resolution); + vec3 albedo = texture(samplerGbufferAlbedo, uv).rgb; + vec2 metallicRoughness = texture(samplerGbufferMetallicRoughness, uv).xy; + float metallic = metallicRoughness.x; + float roughness = metallicRoughness.y; + + // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-pbr_mat + + const vec3 dielectricSpecular = vec3(0.04, 0.04, 0.04); + const vec3 black = vec3(0.0, 0.0, 0.0); + + Material mat; + mat.diffuse = mix(albedo * (vec3(1.0) - dielectricSpecular), black, metallic); + mat.f0 = mix(dielectricSpecular, albedo, metallic); + mat.a = roughness * roughness; + + // restore fragment coordinate from depth + float z = texture(samplerGbufferDepth, uv).x; + vec3 fragCoord = vec3(gl_FragCoord.xy, z); + vec3 pos = screenToEye(fragCoord, ubo.resolution, ubo.invProjection); + + vec3 V = normalize(ubo.camPos - pos); + vec3 N = texture(samplerGbufferNormal, uv).xyz; + + vec3 radiance = vec3(0.0); + for(uint i = 0; i < ubo.lightCount; i++) + { + LightData light = lights[i]; + float dist = distance(light.position, pos); + float attenuation = distanceAttenuation(dist, light.radius); + if(attenuation > 0.0) + { + vec3 L = normalize(light.position - pos); + vec3 radiance_in = light.intensity * attenuation; + float NoL = clamp(dot(N, L), 0.0, 1.0); + if(NoL > 0.0) + radiance += brdf(mat, V, L, N) * radiance_in * NoL; + } + } + + outFragColor = vec4(tonemap(radiance), 1.0); +} diff --git a/res/light/lighting.fragment.spirv b/res/light/lighting.fragment.spirv new file mode 100644 index 0000000000000000000000000000000000000000..9ca1d97ad3df14ad750b119f502b560662a27a53 Binary files /dev/null and b/res/light/lighting.fragment.spirv differ diff --git a/res/light/lighting.vert b/res/light/lighting.vert new file mode 100644 index 0000000000000000000000000000000000000000..7bec00d3a8b52e4969d858e386cc762085e244dc --- /dev/null +++ b/res/light/lighting.vert @@ -0,0 +1,7 @@ +#version 450 core + +void main() +{ + vec2 uv = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); + gl_Position = vec4(uv * 2.0f + -1.0f, 0.0f, 1.0f); +} diff --git a/res/light/lighting.vertex.spirv b/res/light/lighting.vertex.spirv new file mode 100644 index 0000000000000000000000000000000000000000..d664c7747f5c955ac7ff3fcabafc00644bcb5f87 Binary files /dev/null and b/res/light/lighting.vertex.spirv differ diff --git a/res/light/normal.png b/res/light/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..c73c2672b782f9c858189cbf2dad969734435af2 Binary files /dev/null and b/res/light/normal.png differ diff --git a/res/light/roughness.png b/res/light/roughness.png new file mode 100644 index 0000000000000000000000000000000000000000..075f9431c83d4c8caa664530f4d4afc3f5bff086 Binary files /dev/null and b/res/light/roughness.png differ