three.js TSL 环境贴图预处理节点 PMREMNode 完全解析:从 `pmremTexture()` 到 PBR 反射采样
导读
PMREMNode 是 three.js Node Material / TSL 体系中专用于 PBR 环境光照(Image-Based Lighting, IBL) 的预处理节点:它把 HDR 全景/立方体贴图转换成 PMREM(Pre-Filtered, Mip-mapped Radiance Environment Map)专用布局,使 PBR 材质在做环境反射时能按粗糙度即时查到对应的预滤波辐照度。本文以 PMREMNode 官方文档 为骨架,结合 PMREMNode.js 源码 与仓库中的真实 WebGPU 示例,完整讲解它的用法、构造函数参数、公开属性方法,以及“按渲染器缓存、按 pmremVersion 失效、每帧自动再生成”的底层工作机理。读完你将能在 TSL 中直接写出 material.envNode = pmremTexture( envMap ) 式的环境反射方案,并理解其与 WebGL 侧 PMREMGenerator 的关系。
PMREMNode 是什么:面向 PBR 的预处理环境贴图节点
背景:为什么直接采样 HDR 全景图不够
在 PBR 管线中,材质反射需要的不是单一环境颜色,而是“沿反射方向、按当前粗糙度摊开的镜面辐射度”。直接在着色器中对一张全景/立方图做逐像素卷积代价过高且每帧不可重复。业界通用做法是离线/预先生成 PMREM:把环境图按一系列越来越模糊的 mip 层预滤波,粗糙度越大对应采样 mip 越高的模糊层,运行时只需一次纹理采样外加少量计算。
在 three.js 中,传统 WebGL 路径使用 PMREMGenerator 显式生成 cubeUV 渲染目标(见 webgl_pmrem_equirectangular.html);而 PMREMNode 则是 TSL / Node 渲染路径(WebGPURenderer、NodeMaterial)下对这一流程的封装——它把“生成、缓存、采样”全部收敛到一个节点对象里,声明式接入着色器。
官方文档 PMREMNode 页面 对该类的定位是:
This node represents a PMREM which is a special type of preprocessed environment map intended for PBR materials.
继承链为 EventDispatcher → Node → TempNode → PMREMNode(见 docs/pages/PMREMNode.html.md),源码注释中 @augments TempNode(PMREMNode.js)与文档一致。因此它属于“临时节点”,通常经由 TSL 工厂函数 pmremTexture() 创建后直接赋给材质/场景的 env 属性,并不会常驻在节点图之外单独持有。
核心用法(官方 Code Example)
const material = new MeshStandardNodeMaterial();
material.envNode = pmremTexture( envMap );
将环境贴图交给 PMREM 预处理后,作为材质的 envNode 参与镜面反射计算。在仓库示例 webgpu_materials_envmaps_bpcem.html 中可以看到完全一致的用法,并且还展示了把 UV 节点换成自定义法线纠正向量以实现盒型投影(Box-projected)反射:
defaultMat.envNode = pmremTexture( renderTarget.texture );
// 盒型投影环境:pmremTexture 的 uv 参数传入反射方向相关的法线纠正节点
boxProjectedMat.envNode = pmremTexture( renderTarget.texture, getParallaxCorrectNormal( reflectVector, vec3( 200, 200, 100 ), vec3( 0, - 50, 0 ) ) );
需要注意,pmremTexture 与 PMREMNode 所在的 WebGPU/Node 示例全部通过 three/tsl 导入:
import { pmremTexture } from 'three/tsl';
例如 webgpu_pmrem_equirectangular.html:
import { normalWorldGeometry, uniform, pmremTexture } from 'three/tsl';
与其他场景/背景用法的衔接
pmremTexture 同样适合直接赋给场景节点,例如用预处理后的环境做纯反射背景或球体映射:
- webgpu_cubemap_mix.html 把两个 PMREM 用 TSL 运算符混合后赋给
scene.environmentNode:scene.environmentNode = mix( pmremTexture( cube2Texture ), pmremTexture( cube1Texture ), oscSine( time.mul( .1 ) ) ); - webgpu_cubemap_adjustments.html 用
pmremTexture结合saturation/hue等节点对两张 HDR 做逐像素空间混合。
这说明 PMREMNode 返回的是普通 vec3 颜色输出,可以像任何颜色节点一样参与算术、混合、作为 envNode/environmentNode/backgroundNode 使用——它已经替你把“环境贴图预处理”这一步藏在了内部。
构造函数与参数详解
构造函数签名(官方文档 new PMREMNode( value, uvNode, levelNode )):
new PMREMNode( value : Texture, uvNode : Node.<vec2>, levelNode : Node.<float> )
源码实现见 PMREMNode.js:
constructor( value, uvNode = null, levelNode = null ) {
super( 'vec3' );
this._value = value; // 输入纹理
this._pmrem = null; // 已生成的 PMREM
this.uvNode = uvNode; // uv 节点(vec2),默认 null
this.levelNode = levelNode; // level/粗糙度节点(float),默认 null
this._generator = null; // PMREMGenerator,首次 setup 时惰性创建
...
this.updateBeforeType = NodeUpdateType.RENDER; // 'render'
}
value : Texture —— 输入纹理
需要预处理的输入环境贴图。TSL 工厂入口 pmremTexture 声明于 PMREMNode.js:
export const pmremTexture = /*@__PURE__*/ nodeProxy( PMREMNode ).setParameterLength( 1, 3 );
即最少传 1 个参数(value),最多传 3 个(value, uvNode, levelNode)。如果源纹理本身就是 PMREM/满足 cubeUV 约定(texture.isPMREMTexture === true 或 texture.mapping === CubeUVReflectionMapping),节点会直接复用而不重复生成(见 updateBefore 逻辑)。
uvNode : Node. —— 采样方向节点
采样的方向(实为 cubeUV 布局下使用的二维坐标映射),默认 null。
从 PMREMNode.js 看,当 uvNode === null 时会尝试从 builder 上下文取:
let uvNode = this.uvNode;
if ( uvNode === null && builder.context.getUV ) {
uvNode = builder.context.getUV( this, builder );
}
也就是说:当你把 pmremTexture 赋给 material.envNode,node 材质框架会注入合适的反射/视线 UV 节点(如 reflectVector 相关节点);而赋给背景/其他位置时则通常显式传入方向节点(如 webgpu_pmrem_equirectangular.html 中传入 normalWorldGeometry)。
levelNode : Node. —— 粗糙度/采样级别节点
对应采样的 mip 选择输入,决定反射的模糊程度,默认 null。等价地,当为 null 时会从 builder 上下文获取(builder.context.getTextureLevel,见 PMREMNode.js)——例如环境反射会自动带入材质 roughness。自定义时可显式传入 uniform( 0.5 ) 之类的常量或粗糙度表达式。
在 PMREM 采样中,粗糙度通过 roughnessToMip 映射到 mip 层级(详见下文“采样着色器”小节),因此 levelNode 实际承载的是“粗糙度 → mip”的驱动信号。
公开属性
.uvNode : Node.
即构造参数,记录当前使用的采样方向节点。可直接改写实现动态方向。
.levelNode : Node.
记录当前使用的采样级别节点。可直接在运行时替换,从而控制反射模糊程度。
.value : Texture
节点的纹理输入。源码实现为访问器(getter/setter),设置新纹理时会立即作废已生成的 PMREM,强制下一帧重建:
set value( value ) {
this._value = value;
this._pmrem = null; // 触发重新生成
}
get value() {
return this._value;
}
见 PMREMNode.js。若希望交换环境贴图,改 .value 是最干净的入口。
.updateBeforeType : string
取值为 NodeUpdateType.RENDER(字符串 'render'),见 PMREMNode.js。这是文档强调的重点之一:
- 它重写(override)了 TempNode 的
updateBeforeType(官方文档 TempNode#updateBeforeType)。 - 含义:three.js 每帧在正式渲染前都会回调该节点的
updateBefore(),从而检测源纹理是否被替换/重新加载,并据此判断是否需要重新生成 PMREM。这也是为什么 WebGPU 示例中纹理异步加载完成后无需手动“再生成”即可自动呈现的原因。
公开方法
.updateFromTexture( texture : Texture )
用给定的 PMREM 纹理同步内部缓存值。该方法核心逻辑是把 PMREM 的尺寸信息换算成 cubeUV 采样所需的三个 uniform(texel 宽/高与最大 mip),见 PMREMNode.js:
updateFromTexture( texture ) {
const cubeUVSize = _generateCubeUVSize( texture.image.height );
this._texture.value = texture;
this._width.value = cubeUVSize.texelWidth;
this._height.value = cubeUVSize.texelHeight;
this._maxMip.value = cubeUVSize.maxMip;
}
换算规则由私有工具函数 _generateCubeUVSize 定义(PMREMNode.js):
function _generateCubeUVSize( imageHeight ) {
const maxMip = Math.log2( imageHeight ) - 2;
const texelHeight = 1.0 / imageHeight;
const texelWidth = 1.0 / ( 3 * Math.max( Math.pow( 2, maxMip ), 7 * 16 ) );
return { texelWidth, texelHeight, maxMip };
}
可见 PMREM 的内部尺寸完全由生成图的高度推导:maxMip 约为 log2(height) - 2,这与 cubeUV 横排 6 面、外加卷积模糊行的布局约定一致。这些数值随后作为 uniform 传给采样函数 textureCubeUV。
方法本身是公开的,通常无需手动调用——它在 PMREM 生成成功后由内部自动触发;但你也可以用其语义去“喂”一份外部已生成好的 PMREM 纹理(比如来自 PMREMGenerator),让节点跳过自生成。
深入源码:PMREM 是如何按需生成与缓存的
理解 PMREMNode 的“自动预处理”能力,需要看它的两个私有协作函数与 updateBefore/setup 的配合。
每帧失效检测:updateBefore
updateBefore( frame ) {
let pmrem = this._pmrem;
const pmremVersion = pmrem ? pmrem.pmremVersion : - 1;
const texture = this._value;
if ( pmremVersion !== texture.pmremVersion ) {
if ( texture.isPMREMTexture === true || texture.mapping === CubeUVReflectionMapping ) {
pmrem = texture; // 已经是 PMREM,直接复用
} else {
pmrem = _getPMREMFromTexture( texture, frame.renderer, this._generator );
}
if ( pmrem !== null ) {
this._pmrem = pmrem;
this.updateFromTexture( pmrem );
}
}
}
要点:
- 通过版本号
pmremVersion判断源纹理是否变化(如异步加载完成、数据更新)。避免重复卷积。 - 输入若已是 PMREM(
isPMREMTexture)或已采用CubeUVReflectionMapping,直接引用源纹理,不做二次卷积。 - 否则调用
_getPMREMFromTexture走真实生成管线;生成需要渲染器,因此updateBeforeType必须设为每帧渲染前执行,且需要拿到frame.renderer。
按渲染器缓存的生成管线
_getPMREMFromTexture 是生成与缓存的核心(PMREMNode.js):
- 以「渲染器」为维度建立 WeakMap 缓存(
_getCache),每个渲染器各自维护WeakMap<Texture, Texture>,因为 PMREM 本质是 render target 纹理,不能跨渲染上下文共享(源码注释原话,见 PMREMNode.js)。 - 判断输入是
isCubeTexture还是等距柱状投影图,分别调用PMREMGenerator.fromCubemap()或PMREMGenerator.fromEquirectangular();二者执行前都有“图像是否加载完成”守卫(isCubeMapReady要求 6 个面齐全,isEquirectangularMapReady要求image.height > 0),未就绪返回null,等待下一帧重试——这正是异步纹理不显示“黑屏”而是随后自动出现的机理。 - 生成结果打上
pmremVersion标记;首次缓存新 PMREM 时会给源纹理注册dispose监听,纹理销毁时同步释放 PMREM 并清除缓存条目,防止泄漏。
生成器实例 this._generator 采用惰性创建(首次 setup 时 new PMREMGenerator( builder.renderer )),节点销毁时通过 dispose() 释放(PMREMNode.js)。
setup 阶段组装采样
PMREMNode.js 中 setup 完成节点类型标注(super('vec3'))之外的关键装配:
// PMREMGenerator 输出的是 Y 轴翻转的 render target,采样时必须翻转 Y;
// 外部人工制作的 PMREM 遵循标准约定,则不需要翻转。
uvNode = this._pmrem === null || this._pmrem.isRenderTargetTexture
? materialEnvRotation.mul( vec3( uvNode.x, uvNode.y.negate(), uvNode.z ) )
: materialEnvRotation.mul( uvNode );
...
return textureCubeUV( this._texture, uvNode, levelNode, this._width, this._height, this._maxMip );
注意两个实现细节:
- Y 翻转:自动生成的 PMREM 渲染目标存在 Y 轴翻转,采样 UV 需对 Y 取反;而外部提供的 PMREM 纹理(非 render target)无需翻转。此外统一乘以
materialEnvRotation,以支持texture.rotation/ env 旋转控制。 - 最终输出是
textureCubeUV的结果——一个 vec3 颜色节点。
采样着色器:textureCubeUV 与粗糙度→mip 映射
最终采样函数 textureCubeUV 位于同目录 PMREMUtils.js。其输入恰好对应该节点内部持有的三个 uniform(texel 宽/高、最大 mip),逻辑为:
- 将输入的 level/粗糙度映射到 mip:
roughnessToMip依据文档式分段线性映射(粗糙度区间 [0.8,1.0]、[0.4,0.8]、[0.305,0.4]、[0.21,0.305]、其余走-2·log2(1.16·r)对数分支),相关常量cubeUV_r0..r6、cubeUV_m0..m6在 PMREMUtils.js 中定义并注明“与 PMREMGenerator 保持一致”。 - mip 取下限 clamp 到
cubeUV_m0(-2)与CUBEUV_MAX_MIP之间;取mipInt = floor(mip)、mipF = fract(mip)。 - 对相邻两级 mip 各做一次
bilinearCubeUV采样,再用mix(color0, color1, mipF)线性插值,得到带小数层级的平滑模糊结果。
bilinearCubeUV(PMREMUtils.js)实现 cubeUV 布局寻址:将方向向量换算到面索引(getFace)与单面 UV(getUV),依据 mip 与面偏移在横排的立方体贴图纹素中取样,并在采样时显式禁用各向异性过滤(.grad(vec2(), vec2()))以贴合 PMREM 预滤波语义。由于整个寻址与模糊被写成 TSL 的 Fn,这些函数可在 WebGPU 与 WebGL(forceWebGL) 后端一致执行。
实战组合:完整可运行的 WebGPU 示例解读
仓库 webgpu_pmrem_equirectangular.html 是把 PMREMNode 用于场景背景 + 材质环境反射的完整演示,其关键链路:
- 渲染器使用 WebGPURenderer(可
forceWebGL: false)并开启 ACESFilmic 色调映射; - 纹理加载用
UltraHDRLoader加载 2K HDR 等距柱状全景,加载后设map.mapping = THREE.EquirectangularReflectionMapping; - 背景直接用 TSL 节点化表达,且显式传入 uv 与 level:
—— 这里 uvNode 取法线方向几何节点(世界空间法线即采样方向),levelNode 用常量 0.5 固定模糊程度;scene.backgroundNode = pmremTexture( map, normalWorldGeometry, uniform( 0.5 ) ); - 材质侧使用
MeshPhysicalNodeMaterial,传入roughness/metalness网格(5×6 球体矩阵),并把原始 HDR 纹理赋给envMap属性:Node 材质内部会把const material = new THREE.MeshPhysicalNodeMaterial( { roughness: i / 5, metalness: j / 4, envMap: map } );envMap接入PMREMNode驱动的环境反射,配合 per-帧updateBefore的版本检测,纹理加载完成那一刻所有球体自动呈现正确的粗糙度模糊反射。
这个示例很好地印证了文档 Code Example 的工程落地方式:在 Node 体系下你不需要手动调用 PMREMGenerator.fromEquirectangular()(那是 WebGL 传统路径如 webgl_pmrem_equirectangular.html 的做法),把原始环境贴图交给 pmremTexture 即可。
另一个值得对照的示例 webgpu_pmrem_cubemap.html 展示了立方体贴图输入 + 场景环境光照的路径;两者共同点都是输入普通 HDR 环境贴图,差异仅在输入的映射类型(等距柱状 vs 立方体),而 PMREMNode 会根据 texture.isCubeTexture 自动选择 fromEquirectangular 或 fromCubemap。
与 WebGL 经典路径的对照(帮助迁移理解)
如果你熟悉 three.js 传统 WebGL 侧 API,可以用下表快速对齐概念:
| 概念 | WebGL 经典路径 | Node/TSL 路径(本文) |
|---|---|---|
| 生成器 | PMREMGenerator | 节点内部惰性创建的同款生成器 |
| 生成入口 | generator.fromEquirectangular(map) / fromCubemap(map) |
pmremTexture(map) 自动分流 |
| 缓存 | 手动持有 render target | 按渲染器 WeakMap 自动缓存,纹理 dispose 自动清理 |
| 失效 | 手动重生成 | 通过 texture.pmremVersion 每帧检测 |
| 采样 | 运行时内置 shader chunk | textureCubeUV TSL 函数 |
值得强调的是,两套路径共享同一套生成算法与同一份常量约定:PMREMNode 调用的 PMREMGenerator 与 PMREMUtils.js 头部注释“These defines must match with PMREMGenerator”相互印证——着色器侧与生成侧共同遵守 cubeUV 布局的 mip 映射规则,保证预滤波数据能被采样代码正确读取。因此你可以放心把 PMREMGenerator 产出的 PMREM 纹理交给 PMREMNode 消费,updateBefore 中 isPMREMTexture === true 分支会直接复用、跳过二次卷积。
小结:何时该用 PMREMNode / pmremTexture
- 你在使用 WebGPU 渲染器或任意 NodeMaterial(
MeshStandardNodeMaterial、MeshPhysicalNodeMaterial)做 IBL:给material.envNode、scene.environmentNode、scene.backgroundNode赋pmremTexture(...)是最自然的选择,PMREM 的生成、缓存、失效与销毁全部自动完成。 - 输入类型支持等距柱状投影纹理与立方体纹理,节点自动按
isCubeTexture分流生成;若纹理自身已是 PMREM/CubeUVReflectionMapping,则直接采样不复卷积。 - 定制采样通过构造参数
uvNode(采样方向)与levelNode(粗糙度/级别)实现,默认null时会由材质上下文注入(反射向量与材质 roughness)。 - 若你仍在使用传统 WebGL 光照材质(非 Node),请继续使用
PMREMGenerator+CubeTexture/WebGLRenderTarget的经典管线,仓库中的 webgl_materials_envmaps_hdr.html、webgl_materials_envmaps_exr.html 等示例即属此类——它们与本文节点属于两条互补路径。
进一步阅读:官方 API 页 docs/pages/PMREMNode.html.md,配套采样实现 src/nodes/pmrem/PMREMUtils.js,以及 Node 材质环境接入点 src/nodes/accessors/MaterialProperties.js 中的 materialEnvRotation 与相关 lighting 节点。
</||DSML||parameter> </||DSML||invoke> </||DSML||tool_calls>
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 StartedRust0631
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证件照制作算法。Python09
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