Improve PBR quality: mip chains, anisotropic filtering, baseColorFactor

Root causes behind blurry/unphysical materials (4K pocket watch render
mushy, glass fully opaque, watch hands unreadable):

- Textures loaded via stbi had a single mip level: 4096x4096 albedo/MR
  maps sampled with no LOD -> minification aliasing turned fine detail
  (watch hands) into noise. Generate a full blit-based mip chain with
  per-level layout transitions.
- samplerAnisotropy was never enabled on the device and every sampler
  was created with anisotropy disabled: grazing-angle surfaces (watch
  face, ground) collapsed into a blur. Enable the device feature and
  16x anisotropic filtering on material texture samplers.
- glTF baseColorFactor was ignored: the glass material (alphaMode
  BLEND, no baseColor texture, baseColorFactor.a = 0.3) fell back to
  the default white texture with alpha = 1 and rendered fully opaque.
  MaterialParams gains a baseColor vec4 (rgb tints albedo, a scales
  the blend alpha) threaded through both PBR shaders.
- alphaMode MASK was treated as opaque: add a factors.w switch that
  performs an alpha cutout test in the opaque shader.
- Fix material cache key dropping factors.z (separate-roughness
  switch); it now encodes the full factors and baseColor vectors.
This commit is contained in:
2026-10-05 12:01:07 +08:00
parent a3037cd401
commit 5d88ed2057
9 changed files with 124 additions and 23 deletions
@@ -43,7 +43,10 @@ namespace Engine {
// x = metallic 因子 y = roughness 因子
// z = 独立粗糙度图开关(1 = 用 Roughness 槽 R 通道替换 MR.g)
struct MaterialParams {
// x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
glm::vec4 factors{0.0f, 0.5f, 0.0f, 0.0f};
// glTF baseColorFactor:rgb 乘进 albedo,a 作 BLEND 透明度
glm::vec4 baseColor{1.0f, 1.0f, 1.0f, 1.0f};
};
class Material {
+5
View File
@@ -45,6 +45,9 @@ public:
bool IsSynchronization2Supported() const { return m_Synchronization2Supported; }
// MDI:drawCount>1 的 drawIndexedIndirect(Count) 必需(core 特性位)
bool IsMultiDrawIndirectSupported() const { return m_MultiDrawIndirectSupported; }
// 各向异性过滤:纹理采样器清晰度(掠射角材质锐利度)的硬件开关
bool IsSamplerAnisotropySupported() const { return m_SamplerAnisotropySupported; }
float MaxSamplerAnisotropy() const { return m_MaxSamplerAnisotropy; }
private:
void CreateInstance();
@@ -76,6 +79,8 @@ private:
bool m_Synchronization2Supported = false;
bool m_MultiDrawIndirectSupported = false;
bool m_DrawIndirectCountSupported = false;
bool m_SamplerAnisotropySupported = false;
float m_MaxSamplerAnisotropy = 1.0f;
std::vector<const char*> m_ValidationLayers = { "VK_LAYER_KHRONOS_validation" };
std::vector<const char*> m_DeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
+2 -1
View File
@@ -54,7 +54,8 @@ namespace Engine {
Texture(Device& device, MemoryAllocator& allocator, ProceduralTag,
const std::array<u8, 4>& rgba, bool srgb);
// 公共路径:staging → device local image → view/sampler(RGBA8 单 mip)
// 公共路径:staging → device local image → blit 生成完整 mip 链 →
// view/sampler(RGBA8,各向异性过滤)
void InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb);
// DDS 路径:压缩数据直传(BC 硬件采样解码)+ 文件自带 mip 链
+7 -1
View File
@@ -55,7 +55,13 @@ namespace Engine
key += '\n';
}
key += "f:" + std::to_string(params.factors.x) +
"," + std::to_string(params.factors.y);
"," + std::to_string(params.factors.y) +
"," + std::to_string(params.factors.z) +
"," + std::to_string(params.factors.w) +
",c:" + std::to_string(params.baseColor.x) +
"," + std::to_string(params.baseColor.y) +
"," + std::to_string(params.baseColor.z) +
"," + std::to_string(params.baseColor.w);
key += transparent ? ":t" : ":o"; // 透明语义入键(描述符相同但队列不同)
return key;
}
+18 -3
View File
@@ -695,12 +695,27 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
params.factors = glm::vec4(static_cast<f32>(metallic),
static_cast<f32>(roughness), 0.0f, 0.0f);
// glTF alphaMode:BLEND = 半透明(透明绘制队列 + 深度只测不写)。
// MASK(alpha cutout)暂按不透明——需要 alpha test,后续演进。
// baseColorFactor:rgb 调色进 albedo;a 是 BLEND 材质的透明度
//(玻璃等无 baseColor 贴图、纯靠 factor alpha 的材质全靠它,
// 缺了会退化成兜底白纹理的 a=1 = 完全不透)
aiColor4D baseColor(1.0f, 1.0f, 1.0f, 1.0f);
if (material->Get(AI_MATKEY_BASE_COLOR, baseColor) == AI_SUCCESS) {
params.baseColor = glm::vec4(baseColor.r, baseColor.g,
baseColor.b, baseColor.a);
}
// glTF alphaMode:
// BLEND = 半透明(透明绘制队列 + 深度只测不写)
// MASK = 镂空(factors.w=1 → shader 侧 alpha test 丢弃,
// 表针/栅栏等硬边贴图常见)仍走不透明队列
// OBJ/FBX 无此键 → 恒不透明(资产格式定义写死)
aiString alphaMode;
if (material->Get(AI_MATKEY_GLTF_ALPHAMODE, alphaMode) == AI_SUCCESS) {
out.transparent = (alphaMode == aiString("BLEND"));
if (alphaMode == aiString("BLEND")) {
out.transparent = true;
} else if (alphaMode == aiString("MASK")) {
params.factors.w = 1.0f;
}
}
} else {
// FBX/OBJ 独立贴图工作流
+6
View File
@@ -166,6 +166,10 @@ void Device::PickPhysicalDevice()
m_DynamicRenderingSupported = features13.dynamicRendering;
m_Synchronization2Supported = features13.synchronization2;
// 各向异性过滤(core 特性位;桌面 GPU 普遍支持,不支持则采样器保持关闭)
m_SamplerAnisotropySupported = features2.features.samplerAnisotropy == VK_TRUE;
m_MaxSamplerAnisotropy = props.limits.maxSamplerAnisotropy;
// core 特性:MDI(drawCount>1)必需
m_MultiDrawIndirectSupported = features2.features.multiDrawIndirect == VK_TRUE;
// 1.2 特性:drawIndexedIndirectCount 必需(VUID-04445)
@@ -255,6 +259,8 @@ void Device::CreateLogicalDevice()
vk::PhysicalDeviceFeatures features;
// MDI:按材质分组的 drawIndexedIndirectCount,drawCount>1 必需
features.setMultiDrawIndirect(m_MultiDrawIndirectSupported ? VK_TRUE : VK_FALSE);
// 纹理各向异性过滤(材质掠射角清晰度)
features.setSamplerAnisotropy(m_SamplerAnisotropySupported ? VK_TRUE : VK_FALSE);
// ========================================================
// 启用 1.4 特性链
+68 -8
View File
@@ -12,6 +12,7 @@
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstring>
#include "Engine/Core/Stats.h"
@@ -76,7 +77,10 @@ namespace Engine
m_Width = width;
m_Height = height;
m_Format = srgb ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm;
m_MipLevels = 1;
// 完整 mip 链:远处/掠射角采样落到低分辨率层(模糊 + 抗闪烁)。
// 没有它,4K 贴图在小 UV 区域(如表针)采到的是原始分辨率单层
m_MipLevels = 1 + static_cast<u32>(
std::floor(std::log2(std::max(std::max(width, height), 1u))));
vk::DeviceSize imageSize = static_cast<vk::DeviceSize>(width) *
static_cast<vk::DeviceSize>(height) * 4; // RGBA8
@@ -95,11 +99,13 @@ namespace Engine
imageInfo.setImageType(vk::ImageType::e2D)
.setFormat(m_Format)
.setExtent({m_Width, m_Height, 1})
.setMipLevels(1)
.setMipLevels(m_MipLevels)
.setArrayLayers(1)
.setSamples(vk::SampleCountFlagBits::e1)
.setTiling(vk::ImageTiling::eOptimal)
.setUsage(vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled)
.setUsage(vk::ImageUsageFlagBits::eTransferDst |
vk::ImageUsageFlagBits::eTransferSrc |
vk::ImageUsageFlagBits::eSampled)
.setSharingMode(vk::SharingMode::eExclusive)
.setInitialLayout(vk::ImageLayout::eUndefined);
@@ -120,14 +126,17 @@ namespace Engine
m_View = m_Device.GetDevice().createImageView(viewInfo);
// 5.5 创建 Sampler
// 5.5 创建 Sampler(各向异性过滤 = 掠射角材质锐利度关键,如地面/表盘)
const bool af = m_Device.IsSamplerAnisotropySupported();
vk::SamplerCreateInfo samplerInfo;
samplerInfo.setMagFilter(vk::Filter::eLinear)
.setMinFilter(vk::Filter::eLinear)
.setAddressModeU(vk::SamplerAddressMode::eRepeat)
.setAddressModeV(vk::SamplerAddressMode::eRepeat)
.setAddressModeW(vk::SamplerAddressMode::eRepeat)
.setAnisotropyEnable(VK_FALSE)
.setAnisotropyEnable(af ? VK_TRUE : VK_FALSE)
.setMaxAnisotropy(af ? std::min(16.0f, m_Device.MaxSamplerAnisotropy())
: 1.0f)
.setBorderColor(vk::BorderColor::eIntOpaqueBlack)
.setUnnormalizedCoordinates(VK_FALSE)
.setCompareEnable(VK_FALSE)
@@ -182,7 +191,53 @@ namespace Engine
cmd.copyBufferToImage(stagingAlloc.Buffer, m_Image,
vk::ImageLayout::eTransferDstOptimal, region);
// 6.3 布局:TRANSFER_DST → SHADER_READ_ONLY
// 6.3 blit 生成 mip 链:逐级半分辨率缩小(硬件线性滤波)。
// 每级先把上一 mip 转 TRANSFER_SRC(等拷贝/上次 blit 写完),
// blit 出当前 mip,再把上一 mip 转着色器只读;末级单独收尾
i32 mipW = static_cast<i32>(m_Width);
i32 mipH = static_cast<i32>(m_Height);
for (u32 mip = 1; mip < m_MipLevels; ++mip) {
vk::ImageMemoryBarrier2 toSrc;
toSrc.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer)
.setSrcAccessMask(vk::AccessFlagBits2::eTransferWrite)
.setDstStageMask(vk::PipelineStageFlagBits2::eTransfer)
.setDstAccessMask(vk::AccessFlagBits2::eTransferRead)
.setOldLayout(vk::ImageLayout::eTransferDstOptimal)
.setNewLayout(vk::ImageLayout::eTransferSrcOptimal)
.setImage(m_Image)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
mip - 1, 1, 0, 1});
cmd.pipelineBarrier2(vk::DependencyInfo{}.setImageMemoryBarriers(toSrc));
vk::ImageBlit blit;
blit.setSrcOffsets({vk::Offset3D{0, 0, 0}, vk::Offset3D{mipW, mipH, 1}});
blit.setSrcSubresource({vk::ImageAspectFlagBits::eColor, mip - 1, 0, 1});
blit.setDstOffsets({vk::Offset3D{0, 0, 0},
vk::Offset3D{mipW > 1 ? mipW / 2 : 1,
mipH > 1 ? mipH / 2 : 1, 1}});
blit.setDstSubresource({vk::ImageAspectFlagBits::eColor, mip, 0, 1});
cmd.blitImage(m_Image, vk::ImageLayout::eTransferSrcOptimal,
m_Image, vk::ImageLayout::eTransferDstOptimal,
blit, vk::Filter::eLinear);
// 上一级已消费完毕 → 直接转着色器只读
vk::ImageMemoryBarrier2 toRead;
toRead.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer)
.setSrcAccessMask(vk::AccessFlagBits2::eTransferRead)
.setDstStageMask(vk::PipelineStageFlagBits2::eFragmentShader)
.setDstAccessMask(vk::AccessFlagBits2::eShaderRead)
.setOldLayout(vk::ImageLayout::eTransferSrcOptimal)
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
.setImage(m_Image)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
mip - 1, 1, 0, 1});
cmd.pipelineBarrier2(vk::DependencyInfo{}.setImageMemoryBarriers(toRead));
if (mipW > 1) mipW /= 2;
if (mipH > 1) mipH /= 2;
}
// 末级 mip 收尾(blit 目标;单 mip 时即 mip0)→ SHADER_READ_ONLY
vk::ImageMemoryBarrier2 toShader;
toShader.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer)
.setSrcAccessMask(vk::AccessFlagBits2::eTransferWrite)
@@ -191,7 +246,8 @@ namespace Engine
.setOldLayout(vk::ImageLayout::eTransferDstOptimal)
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
.setImage(m_Image)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1});
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
m_MipLevels - 1, 1, 0, 1});
vk::DependencyInfo dep2;
dep2.setImageMemoryBarriers(toShader);
@@ -330,7 +386,11 @@ namespace Engine
.setAddressModeU(vk::SamplerAddressMode::eRepeat)
.setAddressModeV(vk::SamplerAddressMode::eRepeat)
.setAddressModeW(vk::SamplerAddressMode::eRepeat)
.setAnisotropyEnable(VK_FALSE)
.setAnisotropyEnable(m_Device.IsSamplerAnisotropySupported()
? VK_TRUE : VK_FALSE)
.setMaxAnisotropy(m_Device.IsSamplerAnisotropySupported()
? std::min(16.0f, m_Device.MaxSamplerAnisotropy())
: 1.0f)
.setBorderColor(vk::BorderColor::eIntOpaqueBlack)
.setUnnormalizedCoordinates(VK_FALSE)
.setCompareEnable(VK_FALSE)