feat: add simple transparency

This commit is contained in:
2026-10-24 00:36:58 +02:00
parent 0b7cc38543
commit b798ed0378
13 changed files with 488 additions and 18 deletions
@@ -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};
@@ -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<Pipeline> m_pipeline;
std::unique_ptr<Pipeline> m_transparentPipeline;
std::unique_ptr<Pipeline> m_depthPrepassPipeline;
std::unique_ptr<Pipeline> m_selectionPrepassPipeline;
@@ -183,6 +183,7 @@ private:
std::vector<MeshDrawCommand> meshDrawCommands;
std::vector<std::size_t> sortedMeshDrawCommands;
std::vector<std::size_t> transparentMeshDrawCommands;
IndirectDrawList drawList;
DrawStats lastDrawStats;
@@ -414,6 +414,16 @@ void MeshRendererComponent::DrawMaterialEditor(RenderResources& resources, GameR
changed = finished = true;
}
constexpr std::array<const char*, 2> blendModeNames{"Opaque", "Transparent"};
int blendMode = static_cast<int>(material.blendMode);
if (ImGui::Combo("Blend", &blendMode, blendModeNames.data(), static_cast<int>(blendModeNames.size()))) {
material.blendMode = static_cast<BlendMode>(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);
@@ -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{
+3 -9
View File
@@ -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;
@@ -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<Pipeline>(
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();
+54 -6
View File
@@ -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<IndirectDrawList::View> 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);
}
@@ -43,6 +43,18 @@ namespace {
return TextureFilteringMode::Linear;
}
constexpr std::array<std::string_view, 2> 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<BlendMode>(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<std::size_t>(material.textureFilteringMode))},
{"blend", BlendModeNames.at(static_cast<std::size_t>(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");