首页
/ three.js TSL 环境贴图预处理节点 PMREMNode 完全解析:从 `pmremTexture()` 到 PBR 反射采样

three.js TSL 环境贴图预处理节点 PMREMNode 完全解析:从 `pmremTexture()` 到 PBR 反射采样

2026-09-07 23:25:07作者:咎竹峻Karen

导读

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 TempNodePMREMNode.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.htmlpmremTexture 结合 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 === truetexture.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

PMREMNode.js

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 );
        }
    }
}

要点:

  1. 通过版本号 pmremVersion 判断源纹理是否变化(如异步加载完成、数据更新)。避免重复卷积。
  2. 输入若已是 PMREM(isPMREMTexture)或已采用 CubeUVReflectionMapping,直接引用源纹理,不做二次卷积。
  3. 否则调用 _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 采用惰性创建(首次 setupnew PMREMGenerator( builder.renderer )),节点销毁时通过 dispose() 释放(PMREMNode.js)。

setup 阶段组装采样

PMREMNode.jssetup 完成节点类型标注(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),逻辑为:

  1. 将输入的 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..r6cubeUV_m0..m6PMREMUtils.js 中定义并注明“与 PMREMGenerator 保持一致”。
  2. mip 取下限 clamp 到 cubeUV_m0(-2)CUBEUV_MAX_MIP 之间;取 mipInt = floor(mip)mipF = fract(mip)
  3. 对相邻两级 mip 各做一次 bilinearCubeUV 采样,再用 mix(color0, color1, mipF) 线性插值,得到带小数层级的平滑模糊结果。

bilinearCubeUVPMREMUtils.js)实现 cubeUV 布局寻址:将方向向量换算到面索引(getFace)与单面 UV(getUV),依据 mip 与面偏移在横排的立方体贴图纹素中取样,并在采样时显式禁用各向异性过滤(.grad(vec2(), vec2()))以贴合 PMREM 预滤波语义。由于整个寻址与模糊被写成 TSL 的 Fn,这些函数可在 WebGPU 与 WebGL(forceWebGL) 后端一致执行。


实战组合:完整可运行的 WebGPU 示例解读

仓库 webgpu_pmrem_equirectangular.html 是把 PMREMNode 用于场景背景 + 材质环境反射的完整演示,其关键链路:

  1. 渲染器使用 WebGPURenderer(可 forceWebGL: false)并开启 ACESFilmic 色调映射;
  2. 纹理加载UltraHDRLoader 加载 2K HDR 等距柱状全景,加载后设 map.mapping = THREE.EquirectangularReflectionMapping
  3. 背景直接用 TSL 节点化表达,且显式传入 uv 与 level:
    scene.backgroundNode = pmremTexture( map, normalWorldGeometry, uniform( 0.5 ) );
    
    —— 这里 uvNode 取法线方向几何节点(世界空间法线即采样方向),levelNode 用常量 0.5 固定模糊程度;
  4. 材质侧使用 MeshPhysicalNodeMaterial,传入 roughness/metalness 网格(5×6 球体矩阵),并把原始 HDR 纹理赋给 envMap 属性:
    const material = new THREE.MeshPhysicalNodeMaterial( { roughness: i / 5, metalness: j / 4, envMap: map } );
    
    Node 材质内部会把 envMap 接入 PMREMNode 驱动的环境反射,配合 per-帧 updateBefore 的版本检测,纹理加载完成那一刻所有球体自动呈现正确的粗糙度模糊反射。

这个示例很好地印证了文档 Code Example 的工程落地方式:在 Node 体系下你不需要手动调用 PMREMGenerator.fromEquirectangular()(那是 WebGL 传统路径如 webgl_pmrem_equirectangular.html 的做法),把原始环境贴图交给 pmremTexture 即可。

另一个值得对照的示例 webgpu_pmrem_cubemap.html 展示了立方体贴图输入 + 场景环境光照的路径;两者共同点都是输入普通 HDR 环境贴图,差异仅在输入的映射类型(等距柱状 vs 立方体),而 PMREMNode 会根据 texture.isCubeTexture 自动选择 fromEquirectangularfromCubemap


与 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 调用的 PMREMGeneratorPMREMUtils.js 头部注释“These defines must match with PMREMGenerator”相互印证——着色器侧与生成侧共同遵守 cubeUV 布局的 mip 映射规则,保证预滤波数据能被采样代码正确读取。因此你可以放心把 PMREMGenerator 产出的 PMREM 纹理交给 PMREMNode 消费,updateBeforeisPMREMTexture === true 分支会直接复用、跳过二次卷积。


小结:何时该用 PMREMNode / pmremTexture

  • 你在使用 WebGPU 渲染器或任意 NodeMaterial(MeshStandardNodeMaterialMeshPhysicalNodeMaterial)做 IBL:给 material.envNodescene.environmentNodescene.backgroundNodepmremTexture(...) 是最自然的选择,PMREM 的生成、缓存、失效与销毁全部自动完成。
  • 输入类型支持等距柱状投影纹理与立方体纹理,节点自动按 isCubeTexture 分流生成;若纹理自身已是 PMREM/CubeUVReflectionMapping,则直接采样不复卷积。
  • 定制采样通过构造参数 uvNode(采样方向)与 levelNode(粗糙度/级别)实现,默认 null 时会由材质上下文注入(反射向量与材质 roughness)。
  • 若你仍在使用传统 WebGL 光照材质(非 Node),请继续使用 PMREMGenerator + CubeTexture/WebGLRenderTarget 的经典管线,仓库中的 webgl_materials_envmaps_hdr.htmlwebgl_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>

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
858
1.35 K
docsdocs
暂无描述
Markdown
899
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
923
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.83 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
532
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
524
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
393