three.js TSL 的 MaxMipLevelNode 深度解析:如何自动计算纹理的最大 mipmap 级别
MaxMipLevelNode 是 three.js TSL(Three Shading Language)节点系统中一个特殊的 Uniform 节点,其职责是根据纹理的实际尺寸自动计算该纹理的最大 mipmap 级别,并将结果以 uniform 的形式暴露给着色器。该节点是 texture().blur() 模糊采样、textureBicubic() 双三次过滤等高级采样能力的底层支撑。阅读本文后,你将掌握 MaxMipLevelNode 的构造方式、读取属性、每帧更新机制,以及它在 TSL 材质中与 mip 相关的实际工程用途,并能直接在 WebGL/WebGPU 渲染器中复用它。
MaxMipLevelNode 是什么
在 TSL 节点体系里,MaxMipLevelNode 是一条明确可见的继承链上的实现:
EventDispatcher → Node → InputNode → UniformNode → MaxMipLevelNode
即:它是一个输入节点(InputNode)、也是一个均匀值节点(UniformNode),但比普通 uniform 更进一步——它并不要求使用者手动提供值,而是通过自身的 update() 逻辑自动推算纹理的最大 mipmap 级别。其核心实现位于 src/nodes/utils/MaxMipLevelNode.js。
关于其「最大 mipmap 级别」的含义:一张宽 w、高 h 的纹理,其可生成的 mip 链大致为 log2(max(w, h)) 级,MaxMipLevelNode 会把这一数值换算为着色器可用的 mip 上界,供采样偏置、LOD 换算等场景使用。
创建方式与代码示例
创建 MaxMipLevelNode 最直接的方式是通过与其对应的 TSL 工厂函数 maxMipLevel(),它接收一个纹理节点 textureNode:
const level = maxMipLevel( textureNode );
在 MaxMipLevelNode 源码 的尾部可以看到该工厂函数的定义:
export const maxMipLevel = /*@__PURE__*/ nodeProxy( MaxMipLevelNode ).setParameterLength( 1 );
它通过 nodeProxy() 生成,并固定参数长度为 1。maxMipLevel 会被统一导出到 TSL 全局命名空间,因此既可以从 three/tsl 导入(见 src/nodes/TSL.js 中 export * from './utils/MaxMipLevelNode.js'),也可以从 src/Three.TSL.js 引入;若希望直接使用类本身而非 TSL 函数,则可通过 src/nodes/Nodes.js 的 MaxMipLevelNode 具名导出获得。
构造函数:new MaxMipLevelNode( textureNode )
new MaxMipLevelNode( textureNode : TextureNode )
构造一个用于计算指定纹理最大 mip 级别的节点。源码中构造函数的核心逻辑如下(见 MaxMipLevelNode.js):
constructor( textureNode ) {
super( 0 ); // 继承 UniformNode,初始 uniform 值为 0
this._textureNode = textureNode;
this.updateType = NodeUpdateType.FRAME;
}
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
textureNode |
TextureNode |
需要计算最大 mip 级别的纹理节点,对应实际绑定的 three.js 纹理 |
有两个值得注意的实现细节:
super( 0 ):基类UniformNode需要一个初始数值(参见 UniformNode 构造函数,其 value 可以是 JS 基本类型或向量/矩阵/颜色/纹理等 three.js 对象)。这里初始化为0,即在首次执行 update 之前,该 uniform 的值为 0;一旦首帧计算完成,就会被真实的最大 mip 级别所替换。updateType = NodeUpdateType.FRAME:这意味着该节点每一帧都会触发一次update(),从而保证即使纹理尺寸发生变化(例如动态生成的渲染目标纹理),其输出的最大 mip 级别也能保持最新。
属性详解
.texture : Texture(只读)
对外暴露被包装的 three.js 纹理对象。它的实现是一个 getter(见 MaxMipLevelNode.js):
get texture() {
return this._textureNode.value;
}
即把构造时传入的 TextureNode 内部的 .value(真正绑定的 Texture 实例)透传出来。
.textureNode : TextureNode(只读)
用于计算最大 mip 级别的纹理节点本身。通过 getter 返回私有字段 _textureNode(见 MaxMipLevelNode.js)。由于该字段为私有且仅暴露 getter,节点在创建后不允许替换目标纹理。
.updateType : string
节点的更新类型被设置为 NodeUpdateType.FRAME,值为 'frame'。它覆盖(override)了 UniformNode 的默认 updateType,因为该节点需要在每一帧通过其 update() 方法刷新计算。
NodeUpdateType 的完整定义位于 src/nodes/core/constants.js,其取值及语义为:
| 取值 | 常量 | 语义 |
|---|---|---|
'none' |
NodeUpdateType.NONE |
不执行 update() |
'frame' |
NodeUpdateType.FRAME |
每帧执行一次 update() |
'render' |
NodeUpdateType.RENDER |
每次渲染调用执行一次 update()(一帧可能由多次渲染调用组成,比 FRAME 更细粒度) |
'object' |
NodeUpdateType.OBJECT |
每个使用该节点渲染的 Object3D 执行一次 update() |
MaxMipLevelNode 采用 FRAME 而非更细粒度的类型,是因为其目标纹理尺寸在绝大多数场景下是稳定的,每帧刷新一次即可兼顾正确性与开销。
核心原理:update() 如何计算最大 mip 级别
每帧执行的 update() 是理解该节点计算逻辑的关键(见 MaxMipLevelNode.js):
update() {
const texture = this.texture;
const images = texture.images;
const image = ( images && images.length > 0 ) ? ( ( images[ 0 ] && images[ 0 ].image ) || images[ 0 ] ) : texture.image;
if ( image && image.width !== undefined ) {
const { width, height } = image;
this.value = Math.log2( Math.max( width, height ) );
}
}
其计算过程可拆解为三个步骤:
- 定位承载尺寸的 image 对象:对于常规纹理,取
texture.image;对于数组纹理 / 立方体贴图等多图纹理(texture.images存在且非空),则取首张图像,并兼容image内部再嵌套image的数据源(如ImageBitmap包装结构)。 - 读取宽高:从图像对象中取出
width与height。 - 换算 mip 级别:
value = Math.log2( Math.max( width, height ) )。例如一张1024 × 1024的纹理得到10,一张512 × 1024的纹理同样得到10(mip 链长度取决于最长边),而256 × 512则得到9。该值被写入 uniform 的value,从而在着色器中成为一个可参与运算的 float。
与继承基类的协同
作为 UniformNode 的子类,该值在着色器生成阶段通过 UniformNode.generate() 被注册为一个 uniform,并可通过 getPropertyName() 取得其 shader 内名称。这也解释了「将计算逻辑放在 CPU 侧每帧执行,而不是在 GPU 着色器内用 textureQueryLevels 查询」的取舍:CPU 侧一次 log2 计算成本极低,却能以普通 uniform 形式被任意节点流程复用,无需依赖特定的 GLSL/WGSL 内建函数。
在仓库中的实际用途
MaxMipLevelNode 并非一个孤立节点,它被 three.js 内部多个与 mipmap 采样相关的实现复用,这为理解其用途提供了最好的注脚。
1. TextureNode.blur():按比例缩放模糊强度
TextureNode 提供的链式方法 blur( amountNode ) 返回一个经过模糊处理的采样,其实现恰好利用 maxMipLevel 将模糊量归一化到纹理的完整 mip 范围(见 TextureNode.js):
blur( amountNode ) {
const textureNode = this.clone();
textureNode.biasNode = nodeObject( amountNode ).mul( maxMipLevel( textureNode ) );
textureNode.referenceNode = this.getBase();
// ...
return nodeObject( textureNode );
}
即采样偏置 biasNode = amount × maxMipLevel:当 amount 为 0~1 之间的强度值时,偏置从 0 平滑扩展到纹理的 mip 链顶端,amount = 1 即对应全模糊。代码还包含一个工程约束:若纹理未生成 mipmap 或使用了 NearestFilter 等非线性过滤(即 map.generateMipmaps === false),blur() 会给出明确警告并把 biasNode 置回 null,确保采样行为正确。
2. textureBicubic():双三次过滤中的 LOD 换算
在 TextureBicubic.js 中,textureBicubic() 借助最大 mip 级别把模糊强度换算成实际 LOD:
export const textureBicubic = Fn( ( [ textureNode, strength ] ) => {
const lod = strength.mul( maxMipLevel( textureNode ) );
return textureBicubicLevel( textureNode, lod );
} );
其中 strength 是无量纲强度,乘以 maxMipLevel 后才得到该纹理域内真实的 mip 级别,再交由 textureBicubicLevel() 在相邻两级 mip 上执行双三次插值。
3. 可直接参考的示例:反射粗糙度模糊
在真实示例 webgpu_reflection_roughness.html 中可以看到这条调用链的实际落地:
import { Fn, vec2, vec4, texture, uv, textureBicubic, reflector, time } from 'three/tsl';
// ...
const dirtyReflection = textureBicubic( reflection, roughness.mul( .9 ) );
粗糙度越高,双三次模糊采样就越深地取到 mip 链上的低分辨率层,从而得到平滑且无噪声的镜面反射模糊效果——这正是 maxMipLevel 归一化能力在 PBR 材质中的典型体现。
自行使用 MaxMipLevelNode 的建议
在自定义 TSL 材质中,可这样组合使用:
import { texture, uv, maxMipLevel, uniform } from 'three/tsl';
const mapNode = texture( map, uv() ); // 需要 generateMipmaps = true
const level = maxMipLevel( mapNode ); // 每帧自动更新的 uniform float
const partial = mapNode.level( level.mul( strength ) ); // 在最大与最精细 mip 之间采样
使用时有几点来自源码的实践提示:
- 依赖 mipmap:
maxMipLevel的值只有在纹理具备完整 mip 链(texture.generateMipmaps = true且采用LinearFilter等 mip 过滤)时才与实际可采样层级对应;将层级用于level()、bias()前需自行确认这一点,可参考 TextureNode.blur 中的守卫逻辑。 - 每帧自动刷新:由于
updateType = 'frame',当纹理尺寸(如 RTT 结果)改变时无需手动更新 uniform。 - 导入路径:WebGPU/WebGL 项目中统一从
three/tsl导入maxMipLevel工厂函数,即可与本文示例无缝衔接。
相关文档与源码索引
- 节点源码与 TSL 工厂函数:src/nodes/utils/MaxMipLevelNode.js
- 基类实现:src/nodes/core/UniformNode.js(其
updateType语义见 src/nodes/core/constants.js) - 消费方 A:
texture().blur()实现位于 src/nodes/accessors/TextureNode.js - 消费方 B:
textureBicubic()实现位于 src/nodes/accessors/TextureBicubic.js - 运行示例:webgpu_reflection_roughness.html
- 若需查阅节点基类的更完整接口,可参见 UniformNode 文档页 与 TextureNode 文档页
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python08
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00