UE5 ByteCount 源码级开销
2026/9/24 17:46:37 网站建设 项目流程

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 Bytes

3. Single 材质

else if (SubstrateMaterialComplexity.bIsSingle) { // Header SubstrateMaterialRequestedSizeByte += UintByteSize; }

增加:

Single Header:+4 Bytes

F90、Fuzz、SSS 等功能可能使材质进入 Single,但它们不一定各自对应一个独立的+= UintByteSize


4. Custom Encoding 材质

else if (bCustomEncoding) { // Header SubstrateMaterialRequestedSizeByte += UintByteSize; }

增加:

Custom Encoding Header:+4 Bytes

Hair、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 Bytes

6. 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 命中同一个数据块 → +4

10. 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 Bytes

Glint 是当前这些 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 Bytes

14. 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 Bytes

UE5.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 BasesHeader + Normals
14 + 1×4 = 8 Bytes
24 + 2×4 = 12 Bytes
34 + 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。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询