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
@@ -157,6 +157,9 @@ namespace Engine
// 程序化平面(纯色贴图):size 为半边长。带缓存,同参数共享一份。
ModelRef CreatePlane(f32 halfSize, u8 r, u8 g, u8 b);
// 半透明平面(alpha 0-255,透明绘制队列):同参数缓存去重
ModelRef CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a);
// 环境资产(HDR → IBL 预积分四件套)。路径缓存去重,
// 重复 Load 同一路径零成本(动态切换场景直接反复调用)
@@ -37,6 +37,10 @@ namespace Engine::Assets {
// 程序化纯色平面(size 为半边长)
virtual Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) = 0;
// 半透明纯色平面(alpha 0-255,透明绘制队列)
virtual Scope<Model> CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a) = 0;
// 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr)
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
+8 -3
View File
@@ -55,7 +55,8 @@ namespace Engine {
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout layout,
const std::array<Texture*, kSlotCount>& textures,
const MaterialParams& params);
const MaterialParams& params,
bool transparent = false);
~Material();
Material(const Material&) = delete;
@@ -63,15 +64,19 @@ namespace Engine {
vk::DescriptorSet GetDescriptorSet() const { return m_DescriptorSet; }
Texture* GetTexture(Slot slot) const { return m_Textures[slot]; }
// 透明队列归属(glTF alphaMode=BLEND;CPU 收集侧用)
bool IsTransparent() const { return m_Transparent; }
// 材质缓存键:同贴图组合+同因子共享一份(Model 层使用)
// 材质缓存键:同贴图组合+同因子+同透明语义共享一份(Model 层使用)
static std::string MakeCacheKey(const std::string paths[kSlotCount],
const MaterialParams& params);
const MaterialParams& params,
bool transparent);
private:
std::array<Texture*, kSlotCount> m_Textures{};
vk::DescriptorSet m_DescriptorSet;
Scope<UniformBuffer> m_ParamsBuffer; // per-slot 参数 ring(创建时写死)
bool m_Transparent = false;
};
} // namespace Engine
+12 -3
View File
@@ -39,6 +39,7 @@ namespace Engine
std::vector<std::vector<u32>> lods;
std::array<std::string, Material::kSlotCount> texPaths;
MaterialParams params;
bool transparent = false; // glTF alphaMode=BLEND(透明绘制队列)
};
struct ParsedModel {
std::string path;
@@ -74,6 +75,12 @@ namespace Engine
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b);
// 半透明纯色平面(alpha 0-255,预乘 alpha 链):透明管线验证用
static Scope<Model> CreateTransparentPlane(
Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b, u8 a);
~Model();
@@ -89,7 +96,8 @@ namespace Engine
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);
// 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填
Model(Device& device, MemoryAllocator& allocator,
@@ -111,11 +119,12 @@ namespace Engine
Texture* GetOrLoadTexture(const std::string& path, bool srgb);
// 材质缓存入口:同贴图组合+因子共享同一 Material(一个描述符集),
// 材质缓存入口:同贴图组合+同因子+同透明语义共享一份 Material,
// 槽位路径为空/文件缺失折到对应默认纹理。路径已做反斜杠归一化。
// paths 下标 = Material::Slot;params = metallic/roughness 因子
Material* GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
const MaterialParams& params);
const MaterialParams& params,
bool transparent = false);
Device& m_Device;
MemoryAllocator& m_Allocator;
@@ -80,6 +80,8 @@ namespace Engine {
// firstInstance + 连续区间,实体序收集不满足(多实体共享 mesh
// 时实例分散各处),扁平化成连续数组推迟到 appendView
std::vector<std::vector<InstanceMatrices>> batchMats;
// 每 batch 实例距离累加(透明排序用:平均距离降序 = 远→近)
std::vector<f32> batchDistSum;
// batch 查找:数组寻址(下标 = Mesh::GetSlot(),值 = 每 LOD 的
// batchIdx+1,0 = 未用)——逐实例 hash 在大场景是毫秒级开销
std::vector<std::array<u32, kMeshLodCount>> lookup;
@@ -89,11 +91,13 @@ namespace Engine {
result.batches.clear();
result.instanceCount = 0;
batchMats.clear();
batchDistSum.clear();
for (auto& e : lookup) e.fill(0); // 容量保留,置零复用
}
};
ViewScratch m_MainScratch;
ViewScratch m_CascScratch[kMaxCascades];
ViewScratch m_TranslucentScratch; // 透明材质(仅主视图,不投阴影)
// ---- TAA 时域状态 ----
u64 m_FrameIndex = 0; // 抖动序列游标(Halton 2,3 ×8 采样)
@@ -91,6 +91,9 @@ namespace Engine {
// 每级联独立剔除结果(Shadow pass 按 region 各画各的——
// 级联盒沿光方向加了 caster pad,影子不会被剔除裁掉)
CullResult shadowCascades[kMaxCascades];
// 透明批次(预乘 alpha 混合,深度只测不写,远→近排序);
// 不参与阴影投射
CullResult translucentView;
// ---- 运行时开关/调试视图(ImGui 经 Renderer 门面写入,
// SceneRenderer 唯一落帧,各 pass 只读;每帧确定性)----
@@ -35,6 +35,8 @@ namespace Engine {
Scope<Model> LoadModel(const std::string& path,
ParsedModel&& parsed) override;
Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) override;
Scope<Model> CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a) override;
Scope<Cubemap> LoadEnvironment(const std::string& path) override;
Scope<Cubemap> LoadEnvironment(const std::string& path,
DecodedEquirect&& decoded) override;
+3
View File
@@ -31,6 +31,9 @@ namespace Engine {
struct ProceduralTag {};
static Scope<Texture> CreateSolidColor(Device& device, MemoryAllocator& allocator,
u8 r, u8 g, u8 b, bool srgb = true);
// 带 alpha 的纯色纹理(透明管线验证等)
static Scope<Texture> CreateSolidColorRGBA(Device& device, MemoryAllocator& allocator,
u8 r, u8 g, u8 b, u8 a, bool srgb = true);
~Texture();
+12
View File
@@ -41,6 +41,18 @@ namespace Engine::Assets {
[&](const std::string&) { return m_Uploader.CreatePlane(halfSize, r, g, b); });
}
ModelRef AssetManager::CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a)
{
std::string key = "procedural://transparent-plane/" +
std::to_string(halfSize) + "/" +
std::to_string(r) + "," + std::to_string(g) + "," +
std::to_string(b) + "," + std::to_string(a);
return m_Models.Load(key, [&](const std::string&) {
return m_Uploader.CreateTransparentPlane(halfSize, r, g, b, a);
});
}
EnvRef AssetManager::LoadEnvironment(const std::string& path)
{
// GPU 生成(compute 预积分)经接口发生在渲染层实现里
+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
+7
View File
@@ -64,6 +64,13 @@ namespace Engine
{r, g, b, 255}, srgb));
}
Scope<Texture> Texture::CreateSolidColorRGBA(Device& device, MemoryAllocator& allocator,
u8 r, u8 g, u8 b, u8 a, bool srgb)
{
return Scope<Texture>(new Texture(device, allocator, ProceduralTag{},
{r, g, b, a}, srgb));
}
void Texture::InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb)
{
m_Width = width;