feat: 透明材质 alpha blending——独立队列 + 线性 over 混合 + 距离排序

透明来源:glTF alphaMode=BLEND(解析期读取,OBJ/FBX 恒不透明——
资产格式定义写死);MASK 后续演进。Material 加透明语义(入缓存键),
程序化测试资产 CreateTransparentPlane(RGBA 纯色纹理)。

SceneRenderer:透明材质实例收集进独立 scratch(仅主视图,不投
阴影——alpha 阴影后续演进),batch 按平均距离降序排序(远→近,
batchMats 同步重排),SSBO 扁平化顺序 = 排序后顺序。

ForwardPass 两段绘制:不透明(无混合 + 深度写)→ 透明(Shader
Object 动态状态切 SRC_ALPHA/ONE_MINUS_SRC_ALPHA 线性 over + 深度
只测不写);velocity 附件透明段直写(透明表面 velocity 即自身运动)。
frag alpha = albedo 贴图 a 直通,预乘在 tonemap 段(hard constraint
不变)。已知边界:TAA alpha 时域混合(透明边缘几帧渐显);同 batch
多实例不做实例级排序。
This commit is contained in:
2026-09-25 16:57:27 +08:00
parent 8dbfb9f554
commit b25fa29fe5
16 changed files with 213 additions and 27 deletions
+6 -2
View File
@@ -17,8 +17,10 @@ namespace Engine
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout layout,
const std::array<Texture*, kSlotCount>& textures,
const MaterialParams& params)
const MaterialParams& params,
bool transparent)
: m_Textures(textures)
, m_Transparent(transparent)
{
m_DescriptorSet = descriptorManager.AllocateDescriptorSet(layout);
@@ -44,7 +46,8 @@ namespace Engine
Material::~Material() = default;
std::string Material::MakeCacheKey(const std::string paths[kSlotCount],
const MaterialParams& params)
const MaterialParams& params,
bool transparent)
{
std::string key;
for (u32 slot = 0; slot < kSlotCount; ++slot) {
@@ -53,6 +56,7 @@ namespace Engine
}
key += "f:" + std::to_string(params.factors.x) +
"," + std::to_string(params.factors.y);
key += transparent ? ":t" : ":o"; // 透明语义入键(描述符相同但队列不同)
return key;
}
} // namespace Engine
+36 -8
View File
@@ -13,6 +13,7 @@
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include <assimp/GltfMaterial.h> // AI_MATKEY_GLTF_ALPHAMODE
#include <meshoptimizer.h>
@@ -81,15 +82,19 @@ void Model::InitDefaultTextures()
Model::Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
ProceduralTag, f32 halfSize, const std::array<u8, 3>& color)
ProceduralTag, f32 halfSize, const std::array<u8, 4>& rgba,
bool transparent)
: m_Device(device)
, m_Allocator(allocator)
, m_DescriptorManager(descriptorManager)
, m_TextureLayout(textureLayout)
{
// 纯色 1x1 贴图(albedo 兜底)
// 纯色贴图(albedo 兜底;alpha 非全 1 时为半透明材质)
InitDefaultTextures();
auto tex = Texture::CreateSolidColor(device, allocator, color[0], color[1], color[2]);
auto tex = (rgba[3] == 255)
? Texture::CreateSolidColor(device, allocator, rgba[0], rgba[1], rgba[2])
: Texture::CreateSolidColorRGBA(device, allocator,
rgba[0], rgba[1], rgba[2], rgba[3]);
// XZ 平面,法线 +Y,从上往下看 CCW 绕序
const f32 h = halfSize;
@@ -104,7 +109,7 @@ Model::Model(Device& device, MemoryAllocator& allocator,
// 材质/纹理生命周期随模型(缓存持有)
std::string paths[Material::kSlotCount] = {"", "", "", ""};
Material* mat = GetOrCreateMaterial(paths, MaterialParams{}); // 全默认材质
Material* mat = GetOrCreateMaterial(paths, MaterialParams{}, transparent);
m_Meshes.push_back(CreateScope<Mesh>(
m_Device, m_Allocator, vertices, lodIndices, mat));
@@ -118,7 +123,20 @@ Scope<Model> Model::CreatePlane(Device& device, MemoryAllocator& allocator,
{
return Scope<Model>(new Model(device, allocator, descriptorManager,
textureLayout, ProceduralTag{},
halfSize, std::array<u8, 3>{r, g, b}));
halfSize, std::array<u8, 4>{r, g, b, 255},
false));
}
Scope<Model> Model::CreateTransparentPlane(
Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b, u8 a)
{
return Scope<Model>(new Model(device, allocator, descriptorManager,
textureLayout, ProceduralTag{},
halfSize, std::array<u8, 4>{r, g, b, a},
true));
}
ParsedModel Model::ParseFromDisk(const std::string& path)
@@ -245,9 +263,10 @@ Texture* Model::GetOrLoadTexture(const std::string& rawPath, bool srgb)
}
Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
const MaterialParams& params)
const MaterialParams& params,
bool transparent)
{
const std::string key = Material::MakeCacheKey(paths, params);
const std::string key = Material::MakeCacheKey(paths, params, transparent);
if (auto it = m_MaterialCache.find(key); it != m_MaterialCache.end()) {
return it->second.get();
}
@@ -267,7 +286,7 @@ Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCoun
auto mat = CreateScope<Material>(
m_Device, m_Allocator, m_DescriptorManager, m_TextureLayout,
textures, params);
textures, params, transparent);
Material* ptr = mat.get();
m_MaterialCache.emplace(key, std::move(mat));
return ptr;
@@ -391,6 +410,14 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
material->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness);
params.factors = glm::vec4(static_cast<f32>(metallic),
static_cast<f32>(roughness), 0.0f, 0.0f);
// glTF alphaMode:BLEND = 半透明(透明绘制队列 + 深度只测不写)。
// MASK(alpha cutout)暂按不透明——需要 alpha test,后续演进。
// OBJ/FBX 无此键 → 恒不透明(资产格式定义写死)
aiString alphaMode;
if (material->Get(AI_MATKEY_GLTF_ALPHAMODE, alphaMode) == AI_SUCCESS) {
out.transparent = (alphaMode == aiString("BLEND"));
}
} else {
f32 roughnessFactor = 1.0f;
if (texPaths[Material::MetallicRoughness].empty()) {
@@ -409,6 +436,7 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
out.texPaths[slot] = std::move(texPaths[slot]);
}
out.params = params;
// out.transparent 已在 glTF 分支读取
// ---- LOD 链(解析期生成,worker 线程免费时间):----
// LOD0 = 原始;每级目标索引数减半,相对误差 1e-2。顶点共享,
+32 -4
View File
@@ -131,10 +131,7 @@ namespace Engine::Passes
cmd.setColorWriteMaskEXT(0,
std::array<vk::ColorComponentFlags, 2>{colorWriteMask, colorWriteMask});
// ---- 绘制:GPUDriven 按 mesh 分 batch(相机视锥剔除后)----
// 实例矩阵已由 Renderer 写入 ObjectBuffer(per-slot SSBO);
// 同 mesh 的 N 个可见实体合并为一次 drawIndexed(instanceCount=N),
// draw 数 = 可见唯一 mesh 数,与实体数量解耦
// ---- 不透明批次:无混合直写,深度测试 + 写 ----
for (const auto& batch : m_Frame.mainView.batches) {
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 1,
@@ -142,6 +139,37 @@ namespace Engine::Passes
batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
}
// ---- 透明批次:预乘 alpha 混合(线性空间 over),深度只测
// 不写,远 → 近排序(SceneRenderer 侧已按 batch 平均距离排)。
// 附件 0(HDR)混合、附件 1(velocity)直写——透明表面的
// velocity 就是它自身的运动,覆盖自己的像素即正确。
// alpha 语义:frag 输出未预乘线性色 + 覆盖率 alpha,
// 预乘在 tonemap 段(rgb * a)完成 —— hard constraint。
if (!m_Frame.translucentView.batches.empty()) {
vk::ColorBlendEquationEXT premul;
premul.setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha)
.setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha)
.setColorBlendOp(vk::BlendOp::eAdd)
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
.setDstAlphaBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha)
.setAlphaBlendOp(vk::BlendOp::eAdd);
cmd.setColorBlendEnableEXT(0,
std::array<vk::Bool32, 2>{VK_TRUE, VK_FALSE});
cmd.setColorBlendEquationEXT(0,
std::array<vk::ColorBlendEquationEXT, 2>{premul, blendEquation});
cmd.setDepthWriteEnableEXT(VK_FALSE);
for (const auto& batch : m_Frame.translucentView.batches) {
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 1,
batch.mesh->GetMaterial()->GetDescriptorSet(), {});
batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
}
cmd.setDepthWriteEnableEXT(VK_TRUE); // 恢复(下帧 opaque 段也会重设)
}
cmd.endRendering();
})
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment)
+59 -7
View File
@@ -15,6 +15,9 @@
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <algorithm>
#include <numeric>
#include <tracy/Tracy.hpp>
#include <chrono>
@@ -350,23 +353,34 @@ namespace Engine
lodSlots[lod] = batchIdx;
s.result.batches.push_back({mesh.get(), 0, 0, lod});
s.batchMats.emplace_back();
s.batchDistSum.push_back(0.0f);
}
// 实例归入自己的 batch(mesh 分组),保证同 batch
// 实例在扁平化后连续——firstInstance 语义的前提
const u32 bi = batchIdx - 1;
s.result.batches[bi].count++;
s.batchMats[bi].push_back({t.World(), prevWorld});
s.batchDistSum[bi] += dist; // 透明排序用
s.result.instanceCount++;
++totalInstances;
};
tryAppend(m_MainScratch, mainFrustum, mainLod);
for (u32 c = 0; c < kMaxCascades; ++c) {
if (!cascVis[c]) continue; // 粗剔除已判不可见
// 级联降级:越远的级联覆盖范围越大,用越低的 LOD
//(阴影本来就是低频信息,误差可接受);开关关闭 = 恒 LOD0
tryAppend(m_CascScratch[c], cascFrustum[c],
m_LodEnabled ? std::min(c, kMeshLodCount - 1) : 0u);
// 透明材质 → 独立队列(主视图,混合绘制 + 排序;
// 不进阴影——alpha 阴影复杂度高收益低,注释后续演进)
const bool isTransparentMesh =
mesh->GetMaterial()->IsTransparent();
ViewScratch& mainTarget = isTransparentMesh
? m_TranslucentScratch : m_MainScratch;
tryAppend(mainTarget, mainFrustum, mainLod);
if (!isTransparentMesh) {
for (u32 c = 0; c < kMaxCascades; ++c) {
if (!cascVis[c]) continue; // 粗剔除已判不可见
// 级联降级:越远的级联覆盖范围越大,用越低的 LOD
//(阴影本来就是低频信息,误差可接受);开关关闭 = 恒 LOD0
tryAppend(m_CascScratch[c], cascFrustum[c],
m_LodEnabled ? std::min(c, kMeshLodCount - 1) : 0u);
}
}
}
});
@@ -393,6 +407,44 @@ namespace Engine
appendView(m_Frame.shadowCascades[c], m_CascScratch[c]);
}
// ---- 5. 透明批次排序:平均距离降序(远 → 近),连同实例列表同步重排
{
auto& tb = m_TranslucentScratch.result.batches;
if (tb.size() > 1) {
std::vector<u32> order(tb.size());
std::iota(order.begin(), order.end(), 0);
std::sort(order.begin(), order.end(), [&](u32 a, u32 b) {
return m_TranslucentScratch.batchDistSum[a] / tb[a].count >
m_TranslucentScratch.batchDistSum[b] / tb[b].count;
});
CullResult sorted;
sorted.instanceCount = m_TranslucentScratch.result.instanceCount;
std::vector<ViewScratch::InstanceMatrices> sortedMats;
std::vector<f32> sortedDist;
for (const u32 i : order) {
sorted.batches.push_back(tb[i]);
sortedDist.push_back(m_TranslucentScratch.batchDistSum[i]);
for (auto& m : m_TranslucentScratch.batchMats[i]) {
sortedMats.push_back(std::move(m));
}
}
m_TranslucentScratch.result = std::move(sorted);
m_TranslucentScratch.batchDistSum = std::move(sortedDist);
// batchMats 按新分组的 count 顺序切分重装(保留容量)
m_TranslucentScratch.batchMats.assign(
m_TranslucentScratch.result.batches.size(), {});
u32 offset = 0;
for (size_t i = 0; i < m_TranslucentScratch.result.batches.size(); ++i) {
const u32 n = m_TranslucentScratch.result.batches[i].count;
m_TranslucentScratch.batchMats[i].assign(
std::make_move_iterator(sortedMats.begin() + offset),
std::make_move_iterator(sortedMats.begin() + offset + n));
offset += n;
}
}
}
appendView(m_Frame.translucentView, m_TranslucentScratch);
// 帧末滚动时域状态:cur → prev(每帧重建,销毁的实体自动清出)
m_PrevWorlds.swap(m_CurWorlds);
m_CurWorlds.clear();
@@ -29,6 +29,14 @@ namespace Engine {
m_TextureLayout, halfSize, r, g, b);
}
Scope<Model> VulkanAssetUploader::CreateTransparentPlane(
f32 halfSize, u8 r, u8 g, u8 b, u8 a)
{
return Model::CreateTransparentPlane(m_Device, m_Allocator,
m_Descriptors, m_TextureLayout,
halfSize, r, g, b, a);
}
Scope<Cubemap> VulkanAssetUploader::LoadEnvironment(const std::string& path)
{
// compute 预积分(毫秒级):HDR → cube + irradiance/prefilter/LUT