diff --git a/destrum/include/destrum/Graphics/Material.h b/destrum/include/destrum/Graphics/Material.h index 4e28954..be29621 100644 --- a/destrum/include/destrum/Graphics/Material.h +++ b/destrum/include/destrum/Graphics/Material.h @@ -29,6 +29,11 @@ enum class TextureFilteringMode : std::uint32_t { Anisotropic, }; +enum class BlendMode : std::uint32_t { + Opaque, + Transparent, +}; + // Metallic / roughness workflow, matching glTF: // metallic is read from the blue channel, roughness from green and occlusion from red, // so a packed (occlusion)metallicRoughness texture can be used for all three slots. @@ -59,6 +64,9 @@ struct Material { float normalScale{1.f}; float occlusionStrength{1.f}; + BlendMode blendMode{BlendMode::Opaque}; + float opacity{1.f}; + TextureFilteringMode textureFilteringMode{TextureFilteringMode::Linear}; ImageID diffuseTexture{NULL_IMAGE_ID}; diff --git a/destrum/include/destrum/Graphics/Pipelines/MeshPipeline.h b/destrum/include/destrum/Graphics/Pipelines/MeshPipeline.h index fe53b0d..57a4908 100644 --- a/destrum/include/destrum/Graphics/Pipelines/MeshPipeline.h +++ b/destrum/include/destrum/Graphics/Pipelines/MeshPipeline.h @@ -44,6 +44,14 @@ public: const IndirectDrawList& drawList, const IndirectDrawList::View& view); + void drawTransparent( + VkCommandBuffer cmd, + VkExtent2D renderExtent, + const RenderResources& resources, + const GPUBuffer& sceneDataBuffer, + const IndirectDrawList& drawList, + const IndirectDrawList::View& view); + void cleanup(VkDevice device); void setRenderWireframe(bool wireframe) { @@ -72,6 +80,7 @@ private: private: VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE}; std::unique_ptr m_pipeline; + std::unique_ptr m_transparentPipeline; std::unique_ptr m_depthPrepassPipeline; std::unique_ptr m_selectionPrepassPipeline; diff --git a/destrum/include/destrum/Graphics/Renderer.h b/destrum/include/destrum/Graphics/Renderer.h index 6674d41..a67866b 100644 --- a/destrum/include/destrum/Graphics/Renderer.h +++ b/destrum/include/destrum/Graphics/Renderer.h @@ -183,6 +183,7 @@ private: std::vector meshDrawCommands; std::vector sortedMeshDrawCommands; + std::vector transparentMeshDrawCommands; IndirectDrawList drawList; DrawStats lastDrawStats; diff --git a/destrum/src/Components/MeshRendererInspector.cpp b/destrum/src/Components/MeshRendererInspector.cpp index 45c9875..aa2f063 100644 --- a/destrum/src/Components/MeshRendererInspector.cpp +++ b/destrum/src/Components/MeshRendererInspector.cpp @@ -414,6 +414,16 @@ void MeshRendererComponent::DrawMaterialEditor(RenderResources& resources, GameR changed = finished = true; } + constexpr std::array blendModeNames{"Opaque", "Transparent"}; + int blendMode = static_cast(material.blendMode); + if (ImGui::Combo("Blend", &blendMode, blendModeNames.data(), static_cast(blendModeNames.size()))) { + material.blendMode = static_cast(blendMode); + changed = finished = true; + } + if (material.blendMode == BlendMode::Transparent) { + track(ImGui::SliderFloat("Opacity", &material.opacity, 0.0f, 1.0f)); + } + ImGui::SeparatorText("Textures"); for (const TextureSlot& slot : TextureSlots) { ImGui::PushID(slot.label); diff --git a/destrum/src/Graphics/Caches/MaterialCache.cpp b/destrum/src/Graphics/Caches/MaterialCache.cpp index ad2b578..ae25e5e 100644 --- a/destrum/src/Graphics/Caches/MaterialCache.cpp +++ b/destrum/src/Graphics/Caches/MaterialCache.cpp @@ -184,7 +184,7 @@ MaterialData MaterialCache::toMaterialData(const Material& material) const }; return MaterialData{ - .baseColor = glm::vec4(material.baseColor, 1.0f), + .baseColor = glm::vec4(material.baseColor, material.blendMode == BlendMode::Transparent ? material.opacity : 1.0f), .emissiveColor = glm::vec4(material.emissiveColor, 0.0f), .metallicRoughnessNormalOcclusion = glm::vec4{ diff --git a/destrum/src/Graphics/IndirectDrawList.cpp b/destrum/src/Graphics/IndirectDrawList.cpp index 9e8e7cc..3892923 100644 --- a/destrum/src/Graphics/IndirectDrawList.cpp +++ b/destrum/src/Graphics/IndirectDrawList.cpp @@ -97,15 +97,9 @@ IndirectDrawList::View IndirectDrawList::addView( ++view.instanceCount; }; - if (!sortedDrawCommands.empty()) { - for (std::size_t drawIndex : sortedDrawCommands) { - assert(drawIndex < drawCommands.size()); - addDraw(drawCommands[drawIndex]); - } - } else { - for (const MeshDrawCommand& dc : drawCommands) { - addDraw(dc); - } + for (std::size_t drawIndex : sortedDrawCommands) { + assert(drawIndex < drawCommands.size()); + addDraw(drawCommands[drawIndex]); } return view; diff --git a/destrum/src/Graphics/Pipelines/MeshPipeline.cpp b/destrum/src/Graphics/Pipelines/MeshPipeline.cpp index a85e679..bf4bb95 100644 --- a/destrum/src/Graphics/Pipelines/MeshPipeline.cpp +++ b/destrum/src/Graphics/Pipelines/MeshPipeline.cpp @@ -131,6 +131,36 @@ void MeshPipeline::init( pipelineConfig ); } + + { + PipelineConfigInfo pipelineConfig{}; + Pipeline::DefaultPipelineConfigInfo(pipelineConfig); + + pipelineConfig.name = "Mesh Transparent Pipeline"; + pipelineConfig.pipelineLayout = m_pipelineLayout; + + pipelineConfig.vertexAttributeDescriptions = {}; + pipelineConfig.vertexBindingDescriptions = {}; + + pipelineConfig.colorAttachments = {drawImageFormat}; + pipelineConfig.depthAttachment = depthImageFormat; + + pipelineConfig.depthStencilInfo.depthWriteEnable = VK_FALSE; + pipelineConfig.depthStencilInfo.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; + + pipelineConfig.colorBlendAttachment.blendEnable = VK_TRUE; + pipelineConfig.colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + pipelineConfig.colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + pipelineConfig.colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + pipelineConfig.colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + + m_transparentPipeline = std::make_unique( + gfxDevice, + vertexShader.string(), + fragShader.string(), + pipelineConfig + ); + } } catch (...) { @@ -187,6 +217,29 @@ void MeshPipeline::draw( view); } +void MeshPipeline::drawTransparent( + VkCommandBuffer cmd, + VkExtent2D renderExtent, + const RenderResources& resources, + const GPUBuffer& sceneDataBuffer, + const IndirectDrawList& drawList, + const IndirectDrawList::View& view) +{ + if (!m_transparentPipeline || view.commandCount == 0) + { + return; + } + + drawMeshes( + cmd, + *m_transparentPipeline, + renderExtent, + resources, + sceneDataBuffer, + drawList, + view); +} + void MeshPipeline::drawMeshes( VkCommandBuffer cmd, const Pipeline& pipeline, @@ -254,6 +307,7 @@ void MeshPipeline::drawMeshes( void MeshPipeline::cleanup(VkDevice device) { m_pipeline.reset(); + m_transparentPipeline.reset(); m_depthPrepassPipeline.reset(); m_selectionPrepassPipeline.reset(); diff --git a/destrum/src/Graphics/Renderer.cpp b/destrum/src/Graphics/Renderer.cpp index 03bdaf7..24622e3 100644 --- a/destrum/src/Graphics/Renderer.cpp +++ b/destrum/src/Graphics/Renderer.cpp @@ -101,13 +101,24 @@ void GameRenderer::endDrawing() // Grouping by mesh lets IndirectDrawList merge each run into one instanced draw. // Material and vertex buffer are per instance, so they don't break a batch. - std::stable_sort( - sortedMeshDrawCommands.begin(), - sortedMeshDrawCommands.end(), - [this](std::size_t left, std::size_t right) + std::ranges::stable_sort(sortedMeshDrawCommands + , + [this](std::size_t left, std::size_t right) + { + return meshDrawCommands[left].meshId < meshDrawCommands[right].meshId; + }); + + const auto& materials = resources->materials(); + transparentMeshDrawCommands.clear(); + std::erase_if(sortedMeshDrawCommands, [&](std::size_t index) + { + if (materials.getMaterial(meshDrawCommands[index].materialId).blendMode != BlendMode::Transparent) { - return meshDrawCommands[left].meshId < meshDrawCommands[right].meshId; - }); + return false; + } + transparentMeshDrawCommands.push_back(index); + return true; + }); } void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData) @@ -161,7 +172,23 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& // Culling stays on the CPU: every view (camera, directional light, each spot light) // gets its own range of indirect commands in this frame's draw list. + { + ZoneScopedN("Sort Transparent Draws"); + + const glm::vec3 cameraPos = camera.GetPosition(); + const auto distanceSq = [&](std::size_t index) + { + const glm::vec3 offset = meshDrawCommands[index].worldBoundingSphere.center - cameraPos; + return glm::dot(offset, offset); + }; + std::ranges::sort(transparentMeshDrawCommands, [&](std::size_t left, std::size_t right) + { + return distanceSq(left) > distanceSq(right); + }); + } + IndirectDrawList::View mainView; + IndirectDrawList::View transparentView; IndirectDrawList::View directionalShadowView; std::vector spotShadowViews; { @@ -177,6 +204,13 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& edge::createFrustumFromCamera(camera), IndirectDrawList::Filter::All); + transparentView = drawList.addView( + meshes, + meshDrawCommands, + transparentMeshDrawCommands, + edge::createFrustumFromCamera(camera), + IndirectDrawList::Filter::All); + if (shadowLightDirection) { directionalShadowView = drawList.addView( @@ -416,6 +450,19 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& skyboxPipeline->draw(cmd, *resources, camera, sceneData.skyboxRotation); } + { + TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "MeshPipeline::drawTransparent"); + vkutil::ScopedCmdLabel label(cmd, "MeshPipeline::drawTransparent", vkutil::labelColors::opaque); + + meshPipeline->drawTransparent( + cmd, + drawImage.getExtent2D(), + *resources, + sceneDataBuffer.getBuffer(), + drawList, + transparentView); + } + { TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "vkCmdEndRendering"); @@ -600,6 +647,7 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice) pendingMaterialUploads.clear(); meshDrawCommands.clear(); sortedMeshDrawCommands.clear(); + transparentMeshDrawCommands.clear(); initialized = false; GameState::GetInstance().SetRenderer(nullptr); } diff --git a/destrum/src/Serialization/MaterialSerialization.cpp b/destrum/src/Serialization/MaterialSerialization.cpp index c7c2eae..b6c26df 100644 --- a/destrum/src/Serialization/MaterialSerialization.cpp +++ b/destrum/src/Serialization/MaterialSerialization.cpp @@ -43,6 +43,18 @@ namespace { return TextureFilteringMode::Linear; } + constexpr std::array BlendModeNames{"opaque", "transparent"}; + + [[nodiscard]] BlendMode ParseBlendMode(std::string_view name) + { + for (std::size_t i = 0; i < BlendModeNames.size(); ++i) { + if (BlendModeNames[i] == name) { + return static_cast(i); + } + } + return BlendMode::Opaque; + } + [[nodiscard]] RenderResources* GetResources() { auto& state = GameState::GetInstance(); @@ -110,6 +122,8 @@ namespace MaterialSerialization { {"normalScale", material.normalScale}, {"occlusionStrength", material.occlusionStrength}, {"filtering", FilteringNames.at(static_cast(material.textureFilteringMode))}, + {"blend", BlendModeNames.at(static_cast(material.blendMode))}, + {"opacity", material.opacity}, }; json textures = json::object(); @@ -138,6 +152,8 @@ namespace MaterialSerialization { material.normalScale = data.value("normalScale", material.normalScale); material.occlusionStrength = data.value("occlusionStrength", material.occlusionStrength); material.textureFilteringMode = ParseFiltering(data.value("filtering", std::string{"linear"})); + material.blendMode = ParseBlendMode(data.value("blend", std::string{"opaque"})); + material.opacity = data.value("opacity", material.opacity); if (data.contains("textures")) { const json& textures = data.at("textures"); diff --git a/lightkeeper/assets_src/kobe.png b/lightkeeper/assets_src/kobe.png new file mode 100644 index 0000000..87c9482 Binary files /dev/null and b/lightkeeper/assets_src/kobe.png differ diff --git a/lightkeeper/assets_src/kobe.png.meta b/lightkeeper/assets_src/kobe.png.meta new file mode 100644 index 0000000..9ad58e0 --- /dev/null +++ b/lightkeeper/assets_src/kobe.png.meta @@ -0,0 +1,3 @@ +{ + "guid": "a6c64648ac15e99110de13faea6b80e1" +} \ No newline at end of file diff --git a/lightkeeper/assets_src/scenes/main.scene b/lightkeeper/assets_src/scenes/main.scene index 5f9df47..db8919f 100644 --- a/lightkeeper/assets_src/scenes/main.scene +++ b/lightkeeper/assets_src/scenes/main.scene @@ -24,6 +24,7 @@ 1.0, 1.0 ], + "blend": "opaque", "emissiveColor": [ 0.0, 0.0, @@ -34,6 +35,7 @@ "name": "material_0", "normalScale": 1.0, "occlusionStrength": 1.0, + "opacity": 1.0, "roughness": 0.699999988079071, "textures": { "diffuse": { @@ -43,12 +45,40 @@ } } }, + "10": { + "baseColor": [ + 1.0, + 1.0, + 1.0 + ], + "blend": "transparent", + "emissiveColor": [ + 0.0, + 0.0, + 0.0 + ], + "filtering": "linear", + "metallic": 0.0, + "name": "Material", + "normalScale": 1.0, + "occlusionStrength": 1.0, + "opacity": 1.0, + "roughness": 1.0, + "textures": { + "diffuse": { + "guid": "a6c64648ac15e99110de13faea6b80e1", + "path": "game://kobe.png", + "srgb": true + } + } + }, "2": { "baseColor": [ 0.8999999761581421, 0.10000000149011612, 0.10000000149011612 ], + "blend": "opaque", "emissiveColor": [ 0.35999998450279236, 0.04000000283122063, @@ -59,6 +89,7 @@ "name": "ButtonMaterial", "normalScale": 1.0, "occlusionStrength": 1.0, + "opacity": 1.0, "roughness": 0.4000000059604645 }, "3": { @@ -67,6 +98,7 @@ 0.949999988079071, 0.8500000238418579 ], + "blend": "opaque", "emissiveColor": [ 1.0, 0.8199999928474426, @@ -77,6 +109,7 @@ "name": "BulbMaterial", "normalScale": 1.0, "occlusionStrength": 1.0, + "opacity": 1.0, "roughness": 1.0 }, "4": { @@ -85,6 +118,7 @@ 0.8999999761581421, 0.8999999761581421 ], + "blend": "opaque", "emissiveColor": [ 0.0, 0.0, @@ -95,6 +129,7 @@ "name": "LightSwitchMaterial", "normalScale": 1.0, "occlusionStrength": 1.0, + "opacity": 1.0, "roughness": 0.5 }, "5": { @@ -103,6 +138,7 @@ 0.10000000149011612, 0.10000000149011612 ], + "blend": "opaque", "emissiveColor": [ 0.35999998450279236, 0.04000000283122063, @@ -113,7 +149,116 @@ "name": "WinButtonMaterial", "normalScale": 1.0, "occlusionStrength": 1.0, + "opacity": 1.0, "roughness": 0.4000000059604645 + }, + "6": { + "baseColor": [ + 1.0, + 1.0, + 1.0 + ], + "blend": "transparent", + "emissiveColor": [ + 0.0, + 0.0, + 0.0 + ], + "filtering": "linear", + "metallic": 0.0, + "name": "Material", + "normalScale": 1.0, + "occlusionStrength": 1.0, + "opacity": 1.0, + "roughness": 1.0, + "textures": { + "diffuse": { + "guid": "a6c64648ac15e99110de13faea6b80e1", + "path": "game://kobe.png", + "srgb": true + } + } + }, + "7": { + "baseColor": [ + 1.0, + 1.0, + 1.0 + ], + "blend": "transparent", + "emissiveColor": [ + 0.0, + 0.0, + 0.0 + ], + "filtering": "linear", + "metallic": 0.0, + "name": "Material", + "normalScale": 1.0, + "occlusionStrength": 1.0, + "opacity": 1.0, + "roughness": 1.0, + "textures": { + "diffuse": { + "guid": "a6c64648ac15e99110de13faea6b80e1", + "path": "game://kobe.png", + "srgb": true + } + } + }, + "8": { + "baseColor": [ + 1.0, + 1.0, + 1.0 + ], + "blend": "transparent", + "emissiveColor": [ + 0.0, + 0.0, + 0.0 + ], + "filtering": "linear", + "metallic": 0.0, + "name": "Material", + "normalScale": 1.0, + "occlusionStrength": 1.0, + "opacity": 1.0, + "roughness": 1.0, + "textures": { + "diffuse": { + "guid": "a6c64648ac15e99110de13faea6b80e1", + "path": "game://kobe.png", + "srgb": true + } + } + }, + "9": { + "baseColor": [ + 1.0, + 1.0, + 1.0 + ], + "blend": "transparent", + "emissiveColor": [ + 0.0, + 0.0, + 0.0 + ], + "filtering": "linear", + "metallic": 0.0, + "name": "Material", + "normalScale": 1.0, + "occlusionStrength": 1.0, + "opacity": 1.0, + "roughness": 1.0, + "textures": { + "diffuse": { + "guid": "a6c64648ac15e99110de13faea6b80e1", + "path": "game://kobe.png", + "srgb": true + } + } } }, "name": "Main", @@ -7571,7 +7716,6 @@ { "data": { "clip": { - "guid": "b21bcb92ef324239e14c407ad8b9ff39", "path": "game://MontyOnTheRun.wav" }, "loop": false, @@ -7607,6 +7751,186 @@ 1.0 ] } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "localMaterial": "6" + }, + "mesh": "prim://plane?size=1,1" + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 121, + "name": "Decal", + "parent": 0, + "transform": { + "position": [ + -4.743542194366455, + 0.3649294674396515, + 0.07782143354415894 + ], + "rotation": [ + 0.5, + 0.5, + -0.5, + 0.5 + ], + "scale": [ + 1.0, + 0.9999995231628418, + 0.9999995827674866 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "localMaterial": "7" + }, + "mesh": "prim://plane?size=1,1" + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 122, + "name": "Decal", + "parent": 0, + "transform": { + "position": [ + -4.743542194366455, + 1.1351869106292725, + 2.188525676727295 + ], + "rotation": [ + 0.5, + 0.5, + -0.5, + 0.5 + ], + "scale": [ + 1.0, + 0.9999995231628418, + 0.9999995827674866 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "localMaterial": "8" + }, + "mesh": "prim://plane?size=1,1" + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 123, + "name": "Decal", + "parent": 0, + "transform": { + "position": [ + -4.743542194366455, + 1.5165855884552002, + 0.601291835308075 + ], + "rotation": [ + 0.5, + 0.5, + -0.5, + 0.5 + ], + "scale": [ + 1.0, + 0.9999995231628418, + 0.9999995827674866 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "localMaterial": "9" + }, + "mesh": "prim://plane?size=1,1" + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 124, + "name": "Decal", + "parent": 0, + "transform": { + "position": [ + -4.743542194366455, + 0.5876348614692688, + -1.545495867729187 + ], + "rotation": [ + 0.5, + 0.5, + -0.5, + 0.5 + ], + "scale": [ + 1.0, + 0.9999995231628418, + 0.9999995827674866 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "localMaterial": "10" + }, + "mesh": "prim://plane?size=1,1" + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 125, + "name": "Decal", + "parent": 0, + "transform": { + "position": [ + -4.743542194366455, + 1.3765578269958496, + -2.816664457321167 + ], + "rotation": [ + 0.5, + 0.5, + -0.5, + 0.5 + ], + "scale": [ + 1.0, + 0.9999995231628418, + 0.9999995827674866 + ] + } } ], "version": 3 diff --git a/tests/serialization_tests.cpp b/tests/serialization_tests.cpp index 26a92a4..f5ecd71 100644 --- a/tests/serialization_tests.cpp +++ b/tests/serialization_tests.cpp @@ -293,6 +293,8 @@ namespace { material.roughnessFactor = 0.55f; material.emissiveColor = {1.0f, 0.5f, 0.0f}; material.textureFilteringMode = TextureFilteringMode::Anisotropic; + material.blendMode = BlendMode::Transparent; + material.opacity = 0.25f; material.diffuseTexture = 3; material.emissiveTexture = 7; material.normalTexture = 9; @@ -315,7 +317,8 @@ namespace { check(loaded.name == "Crate" && loaded.baseColor == material.baseColor && close(loaded.metallicFactor, 0.3f) && close(loaded.roughnessFactor, 0.55f) && loaded.emissiveColor == material.emissiveColor && - loaded.textureFilteringMode == TextureFilteringMode::Anisotropic, + loaded.textureFilteringMode == TextureFilteringMode::Anisotropic && + loaded.blendMode == BlendMode::Transparent && close(loaded.opacity, 0.25f), "material factors must round-trip through json"); check(loaded.diffuseTexture == 30 && loaded.emissiveTexture == 70 && loaded.normalTexture == NULL_IMAGE_ID && loaded.metallicTexture == NULL_IMAGE_ID,