1. Complex 材质公共开销
if (!SubstrateMaterialComplexity.bIsSimple && !SubstrateMaterialComplexity.bIsSingle && !bCustomEncoding && !bIsFastWaterPath) { // Packed Header SubstrateMaterialRequestedSizeByte += UintByteSize; // Shared Local Bases SubstrateMaterialRequestedSizeByte += UsedSharedLocalBasesCount * SUBSTRATE_PACKED_SHAREDLOCALBASIS_STRIDE_BYTES; }增加:
Packed Header:+4 Bytes Shared Local Basis: UsedSharedLocalBasesCount × BasisStride触发条件:
不是 Simple 不是 Single 不是 Custom Encoding 不是 Fast Water这部分是 Complex 材质的公共成本,不属于某一个 Feature。
2. Simple 材质
if (SubstrateMaterialComplexity.bIsSimple) { // Header SubstrateMaterialRequestedSizeByte += UintByteSize; // Disney material SubstrateMaterialRequestedSizeByte += UintByteSize; break; }增加:
Simple Header: +4 Bytes Disney Material: +4 Bytes 总计: +8 Bytes3. Single 材质
else if (SubstrateMaterialComplexity.bIsSingle) { // Header SubstrateMaterialRequestedSizeByte += UintByteSize; }增加:
Single Header:+4 BytesF90、Fuzz、SSS 等功能可能使材质进入 Single,但它们不一定各自对应一个独立的+= UintByteSize。
4. Custom Encoding 材质
else if (bCustomEncoding) { // Header SubstrateMaterialRequestedSizeByte += UintByteSize; }增加:
Custom Encoding Header:+4 BytesHair、Eye 等特殊 BSDF 会走这类路径。
5. Fast Water 路径
else if (bIsFastWaterPath) { // Header + Data SubstrateMaterialRequestedSizeByte += UintByteSize; // Data SubstrateMaterialRequestedSizeByte += UintByteSize; break; }增加:
Water Header:+4 Bytes Water Data: +4 Bytes 总计: +8 Bytes6. Complex 材质的 Weight 数据
顶层 Slab
else { // BSDF state with gray scale weight SubstrateMaterialRequestedSizeByte += UintByteSize; }增加:
BSDF State + 灰度 Weight:+4 Bytes非顶层 Slab
else if (bMayHaveColoredWeight) { // BSDF state SubstrateMaterialRequestedSizeByte += UintByteSize; // Color weight SubstrateMaterialRequestedSizeByte += UintByteSize; // Light transmittance weight SubstrateMaterialRequestedSizeByte += UintByteSize; }增加:
BSDF State: +4 Bytes Color Weight: +4 Bytes Light Transmittance Weight: +4 Bytes 总计: +12 Bytes所以同样的 Slab,放在顶部和底部,Byte Count 可能不同。
7. Slab 基础数据
进入:
case SUBSTRATE_BSDF_TYPE_SLAB:后通常会先增加:
SubstrateMaterialRequestedSizeByte += UintByteSize; SubstrateMaterialRequestedSizeByte += UintByteSize;增加:
Slab 基础数据:+8 Bytes这不是某个 Feature 的专属成本,而是 Slab 的基础 Payload。
8. Edge Color、Second Roughness、Simple Clear Coat
if (It.Has(ESubstrateBsdfFeature::EdgeColor) || It.Has(ESubstrateBsdfFeature::SecondRoughness) || It.Has(ESubstrateBsdfFeature::SimpleClearCoat)) { SubstrateMaterialRequestedSizeByte += UintByteSize; }增加:
额外 Slab 参数块:+4 Bytes重点是这几个条件使用了||:
Edge Color Second Roughness Simple Clear Coat它们命中任意一个,通常只增加一个数据块,不是三个 Feature 分别收费。
9. 底层 SSS 或非底层 MFP
源码会先判断类似:
const bool bHasSSS = It.bIsBottom && It.Has(ESubstrateBsdfFeature::SSS); const bool bIsSimpleVolume = !It.bIsBottom && It.Has(ESubstrateBsdfFeature::MFP);然后:
if (bHasSSS || bIsSimpleVolume) { SubstrateMaterialRequestedSizeByte += UintByteSize; }增加:
底层 SSS: +4 Bytes 非底层 MFP Simple Volume:+4 Bytes但两者共用同一个条件块:
bHasSSS || bIsSimpleVolume所以不是:
SSS +4 MFP +4而是:
SSS 或 MFP 命中同一个数据块 → +410. Fuzz
if (It.Has(ESubstrateBsdfFeature::Fuzz)) { SubstrateMaterialRequestedSizeByte += UintByteSize; }增加:
Fuzz 数据:+4 Bytes但 Fuzz 还可能改变材质的 Simple/Single/Complex 分类,因此实际总成本可能不只是 4 Bytes。
11. Glint
if (It.Has(ESubstrateBsdfFeature::Glint)) { SubstrateMaterialRequestedSizeByte += UintByteSize; SubstrateMaterialRequestedSizeByte += UintByteSize; SubstrateMaterialRequestedSizeByte += UintByteSize; SubstrateMaterialRequestedSizeByte += UintByteSize; }增加:
Glint:4 × 4 Bytes = +16 BytesGlint 是当前这些 Feature 中直接增加 Payload 较多的一类。
12. Specular Profile
if (It.Has(ESubstrateBsdfFeature::SpecularProfile)) { SubstrateMaterialRequestedSizeByte += UintByteSize; }增加:
Specular Profile:+4 Bytes但 Specular Profile 还会使材质进入 Complex Special 路径,因此它的最终影响不一定只有 4 Bytes。
13. Hair
case SUBSTRATE_BSDF_TYPE_HAIR: { SubstrateMaterialRequestedSizeByte += UintByteSize; SubstrateMaterialRequestedSizeByte += UintByteSize; break; }增加:
Hair Custom Data:+8 Bytes14. Eye
case SUBSTRATE_BSDF_TYPE_EYE: { SubstrateMaterialRequestedSizeByte += UintByteSize; SubstrateMaterialRequestedSizeByte += UintByteSize; break; }增加:
Eye Custom Data:+8 Bytes如果 Eye 使用额外自定义参数,还会继续增加:
if (It.Has(ESubstrateBsdfFeature::ExCustomParameter)) { SubstrateMaterialRequestedSizeByte += UintByteSize; }即:
每个额外自定义数据块:+4 BytesUE5.7 的源码分析中也明确展示了 Eye Custom Encoding 会在HLSLMaterialTranslator.cpp中增加额外 uint 数据。UE5.7.1 源码分析
15. Unlit
Unlit 不会像 Slab 一样增加普通材质 Payload:
case SUBSTRATE_BSDF_TYPE_UNLIT: { // Never stored, it goes directly into the scene as emitted luminance. break; }也就是:
Unlit 不需要存储完整 Slab 数据汇总
| 逻辑 | 增加量 |
|---|---|
| Complex Packed Header | +4 Bytes |
| Shared Local Basis | 数量 × BasisStride |
| Simple Header | +4 Bytes |
| Simple Disney Material | +4 Bytes |
| Single Header | +4 Bytes |
| Custom Encoding Header | +4 Bytes |
| Fast Water Header | +4 Bytes |
| Fast Water Data | +4 Bytes |
| 顶层灰度 Weight | +4 Bytes |
| 非顶层彩色 Weight | +12 Bytes |
| Slab 基础数据 | +8 Bytes |
| Edge Color / Second Roughness / Clear Coat | +4 Bytes |
| 底层 SSS 或非底层 MFP | +4 Bytes |
| Fuzz | +4 Bytes |
| Glint | +16 Bytes |
| Specular Profile | +4 Bytes |
| Hair Custom Data | +8 Bytes |
| Eye Custom Data | +8 Bytes |
| Eye 额外自定义参数 | 每项 +4 Bytes |
不应直接理解为 Byte Count 增量的内容
下面这些通常不是直接的+= UintByteSize,但会改变最终结果:
F90 Anisotropy Shared Local Basis 数量 材质是顶层还是底层 参数混合 材质简化 Simple / Single / Complex 分类 Complex Special 路径 uint32 对齐最后还会进行类似:
RequestedSizeInUint = DivideAndRoundUp( SubstrateMaterialRequestedSizeByte, 4u);因此最终 Byte Count 的正确理解是:
最终 Byte Count = 公共布局 + BSDF 状态 + Slab 基础数据 + Feature 数据块 + Shared Local Basis + 对齐1.bCustomEncoding
表示:
当前 BSDF 不使用通用 Slab 编码,而是使用自己的专用编码方式。
典型类型:
Substrate Hair BSDF Substrate Eye BSDF它们的参数结构和 Slab 不一样,所以不能直接按普通 Slab 处理。
逻辑通常类似:
if (BSDFType == SUBSTRATE_BSDF_TYPE_HAIR || BSDFType == SUBSTRATE_BSDF_TYPE_EYE) { bCustomEncoding = true; }影响:
不走普通 Complex Header 不走普通 Slab Payload 不按 Slab 的 Fuzz / SSS / MFP 分支计算 进入 Hair / Eye 自己的编码逻辑例如:
case SUBSTRATE_BSDF_TYPE_EYE: += UintByteSize; += UintByteSize;所以bCustomEncoding不是“增加一个 Feature”,而是:
切换到另一套 BSDF 存储格式2.bIsFastWaterPath
表示:
当前材质是 Single Layer Water,并且可以使用专用的快速水体路径。
对应:
Substrate SingleLayerWater BSDF它不是:
Slab + MFP而是一个独立的特殊 BSDF。
它通常走:
else if (bIsFastWaterPath) { // Header + Data SubstrateMaterialRequestedSizeByte += UintByteSize; // Data SubstrateMaterialRequestedSizeByte += UintByteSize; break; }也就是:
Water Header:+4 Bytes Water Data: +4 Bytes 总计: +8 Bytes它会跳过:
普通 Slab 数据 Complex Shared Local Basis 普通 Slab Feature 分支所以它是一条专门优化过的水体编码路径。
注意:
Substrate SingleLayerWater BSDF和:
Slab 连接 MFP 做 Simple Volume不是同一种路径。
3、Colored Weight 使用条件
Colored Weight不是由 MFP 是否为 RGB 直接决定的,而主要由材质树拓扑决定。
会使用或预留 Colored Weight 的情况
存在 Vertical Layer 并且当前 Slab 不是顶层 Slab源码逻辑通常是:
bMayHaveColoredWeight = !It.bIsTop;非顶层 Slab 可能受到上层材质的:
彩色透射 颜色吸收 Diffuse / F0 颜色影响 Light Transmittance先把所有增加byte count的代码先汇总于此:
if (!SubstrateMaterialComplexity.bIsSimple && !SubstrateMaterialComplexity.bIsSingle && !bCustomEncoding && !bIsFastWaterPath) // header written later, { // Packed Header SubstrateMaterialRequestedSizeByte += UintByteSize; // Shared local bases between BSDFs SubstrateMaterialRequestedSizeByte += UsedSharedLocalBasesCount * SUBSTRATE_PACKED_SHAREDLOCALBASIS_STRIDE_BYTES; } switch (It.OperatorType) { case SUBSTRATE_OPERATOR_BSDF: { // we have encountered a new BSDF which directly link to a single closure evaluation SubstrateMaterialClosureCount++; // From the compiler side, we can only assume the top layer has gray scale luminance weight. const bool bMayHaveColoredWeight = !It.bIsTop; if (SubstrateMaterialComplexity.bIsSimple) { // Header SubstrateMaterialRequestedSizeByte += UintByteSize; // Disney material SubstrateMaterialRequestedSizeByte += UintByteSize; break; // Stop here } else if (SubstrateMaterialComplexity.bIsSingle) { // Header SubstrateMaterialRequestedSizeByte += UintByteSize; } else if (bCustomEncoding) { // Header SubstrateMaterialRequestedSizeByte += UintByteSize; } else if (bIsFastWaterPath) { // Header + Data SubstrateMaterialRequestedSizeByte += UintByteSize; // Data SubstrateMaterialRequestedSizeByte += UintByteSize; break; // Stop here } else if (bMayHaveColoredWeight) { // BSDF state SubstrateMaterialRequestedSizeByte += UintByteSize; // Color weight SubstrateMaterialRequestedSizeByte += UintByteSize; // Light transmittance weight SubstrateMaterialRequestedSizeByte += UintByteSize; } else { // BSDF state with gray scale weight SubstrateMaterialRequestedSizeByte += UintByteSize; } switch (It.BSDFType) { case SUBSTRATE_BSDF_TYPE_SLAB: { // Compute values closer to the reality for HasSSS and IsSimpleVolume, now that we know that we know the topology of the material. const bool bIsSimpleVolume = !It.bIsBottom && It.Has(ESubstrateBsdfFeature::MFPPluggedIn); const bool bHasSSS = It.bIsBottom && It.Has(ESubstrateBsdfFeature::SSS); SubstrateMaterialRequestedSizeByte += UintByteSize; SubstrateMaterialRequestedSizeByte += UintByteSize; // When using blendable GBuffer, do not account for features's byte requests. as downcasting is done SubstrateExport(). // Otherwise this would cause material to not compile as we do not demote feature during simplification. if (Substrate::IsSubstrateBlendableGBufferEnabled(ShaderPlatform)) { break; } if (It.Has(ESubstrateBsdfFeature::EdgeColor | ESubstrateBsdfFeature::SecondRoughnessOrSimpleClearCoat)) { SubstrateMaterialRequestedSizeByte += UintByteSize; } if (bHasSSS || bIsSimpleVolume) { SubstrateMaterialRequestedSizeByte += UintByteSize; } if (It.Has(ESubstrateBsdfFeature::Fuzz)) { SubstrateMaterialRequestedSizeByte += UintByteSize; } if (It.Has(ESubstrateBsdfFeature::Glint) && Substrate::IsGlintEnabled(ShaderPlatform)) { SubstrateMaterialRequestedSizeByte += UintByteSize; SubstrateMaterialRequestedSizeByte += UintByteSize; SubstrateMaterialRequestedSizeByte += UintByteSize; SubstrateMaterialRequestedSizeByte += UintByteSize; } if (It.Has(ESubstrateBsdfFeature::SpecularProfile)) { SubstrateMaterialRequestedSizeByte += UintByteSize; } if (It.Has(ESubstrateBsdfFeature::Eye)) { SubstrateMaterialRequestedSizeByte += UintByteSize; SubstrateMaterialRequestedSizeByte += UintByteSize; } if (It.Has(ESubstrateBsdfFeature::Hair)) { SubstrateMaterialRequestedSizeByte += UintByteSize; SubstrateMaterialRequestedSizeByte += UintByteSize; } break; } case SUBSTRATE_BSDF_TYPE_HAIR: { // Custom encoding SubstrateMaterialRequestedSizeByte += UintByteSize; SubstrateMaterialRequestedSizeByte += UintByteSize; break; } case SUBSTRATE_BSDF_TYPE_EYE: { // Custom encoding SubstrateMaterialRequestedSizeByte += UintByteSize; SubstrateMaterialRequestedSizeByte += UintByteSize; break; }我来计算一下Glint的byte count:
首先如图一所示,Glint属于Complex Special,这里是要进去的,所以是4 + 4 = 8
然后如图二,它属于else层级的只加4:8+4=12
如图三,它属于slab所以是6*4 = 24 然后24+ 12等于36
计算正确!
再来计算只连接anisotropy消耗多少byte count:
首先连接了anisotropy了过后complexity就属于complex了
那么这里就会2*4=8的byte count消耗了
这里不属于任何一个标识直接加4,目前8+4=12byte count消耗
最后只有基础slab数据的开销2*4=8,总消耗8+12 =20byte count消耗
再来看为什么glint + anisotropy不增加各向异性
让我们来看为什么anisotropy + glint还是36呢
首先,anisotroyp + glint还是 complex special,一样要计算这里header的消耗 2*4=8
老样子走else不涉及任何的分支标识
最后这里还是只进入这几个标识6*4= 24 ,总共24+8+8
其实最核心的就是因为glint和glint+anisotropy的slab类型相同,complexity都是complex special,新增的anisotropy没有对应feature增加的byte count的开销,所以这两个类型最终的总开销是相同的
当 Glint 已经把材质推入 Complex Special,并且 Shared Local Basis 数量没有增加时,再加入 Anisotropy 不会带来额外的 Byte Count;它只改变局部 Basis 的类型和内容。
最后来一个复合类型的
首先上下层覆合关系是complex的,所以里面2*4=8的byte count消耗
上面的SubstrateMaterialComplexity是整个材质的MaterialComplexity而不是单个slab的complextiy
然后,上层slab的operator用的else+4,下层属于colorweight的+3*4=12
最后,首先两个slab都要有基础slab信息,4*4=16,然后上层slab强行变成了issimplevolume,所以再来个4
总共计算byte count:8+4+12+16+4=44
主要第一看tree header和local basis,然后看operator,最后看slab
这里特别注意,这个usesharedlocalbasescount和sharedlocalbasescount没关系,别被坑了
这里是operator有一个类型是bsdf的,那就+1,就是说有多少没被优化的bsdf slab板
实际
SharedLocalBasesCount会影响视口里的Memory Transactions,但不会影响材质编辑器显示的Byte Count。
原因是两处使用了不同的数量。
材质编辑器 Byte Count
计算 Byte Count 时,Normal/Tangent 还没有完成编译,无法进行真正的 Basis 去重,因此源码直接使用:
UsedSharedLocalBasesCount = SubstrateMaterialEffectiveClosureCount;也就是保守假设:
一个有效 Closure = 一个 Local Basis所以只要 Closure Count 不变,实际 Local Bases 从 3 合并为 1,编辑器 Byte Count 仍然不变。
视口 Memory Transactions
最终 Shader 编译完成后,UE 已经能够比较 Normal/Tangent 的代码哈希,并得到:
FinalUsedSharedLocalBasesCount运行时 Header 保存的是这个实际数量:
HEADER_SETSHAREDLOCALBASESCOUNT( Out, InHeader.SharedLocalBases.Count);视口再根据实际数量写入和读取 Basis:
Header + Normals = Packed Header 4 Bytes + SharedLocalBasesCount × 4 Bytes因此:
| 实际 Local Bases | Header + Normals |
|---|---|
| 1 | 4 + 1×4 = 8 Bytes |
| 2 | 4 + 2×4 = 12 Bytes |
| 3 | 4 + 3×4 = 16 Bytes |
每合并一个 Local Basis,实际Memory Transactions就减少:
4 Bytes / pixel前提是其他 BSDF 数据和可见 Closure 没有变化。
所以性能规划里建议明确区分:
Editor Byte Count = 编译期保守预算,用于预算检查和材质简化 Viewport Memory Transactions = 当前像素实际读取的 Substrate 数据量 Closure Count = 实际需要求值的 BSDF 数量 SharedLocalBasesCount = 实际唯一 Normal/Tangent Basis 数量你的核心结论完全正确:复用相同 Normal/Tangent 是有效优化,只是 UE5.7 材质编辑器的 Byte Count 暂时没有体现这项收益;收益会在最终 Shader 和视口实际 Memory Transactions 中体现出来。
右侧Memory Transactions - Total 44就是当前选中像素实际读取了 44 Bytes。源码用的是SubstrateAddressing.ReadBytes
这张图里总差值是:
材质编辑器 Byte Count 76 Memory Transactions 60 总差值 16 Bytes其中:
Shared Local Basis 合并节省 8 Bytes 两个非顶层灰度权重节省 8 Bytes灰度 Weight 被压进BSDF.State的 10-bit 字段,不需要单独存储 Color Weight。