three.js 节点材质中的视口深度纹理节点:ViewportDepthTextureNode 原理与实战指南
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
本篇技术指南聚焦 three.js(当前仓库)节点材质(Node Material / TSL)体系中的ViewportDepthTextureNode,讲解如何把当前视口(viewport)的深度信息作为纹理采样,实现折射保护、软粒子、深度判断等需要“读取场景深度”的高级效果。读完本文,你将掌握该节点的类继承关系、构造参数语义、共享深度缓冲的缓存机制,以及基于 TSL 的viewportDepthTexture()便捷函数在真实示例中的调用方式。
一、节点是什么:把“当前视口深度”变成一个可采样的纹理
ViewportDepthTextureNode表示当前视口的深度,并以纹理形式暴露给着色器使用。文档对它的定位是一句话:Represents the depth of the current viewport as a texture. This module can be used in combination with viewport texture to achieve effects that require depth evaluation.(见 docs/pages/ViewportDepthTextureNode.html.md)
这句话包含两层关键信息:
- 数据来源是“当前视口”:它读的不是外部传入的深度贴图,而是当前正在渲染的 framebuffer(离屏目标或默认画布)中已有的深度缓冲;
- 用途是“配合视口颜色纹理做深度求值”:很多后处理/材质效果(折射、软粒子、接触阴影等)不仅需要看到当前画面颜色,还需要知道每个像素背后的场景离相机有多远。把深度做成纹理后,就能在片元着色器里对深度进行采样和比较。
在继承链上,该节点位于:EventDispatcher → Node → InputNode → UniformNode → TextureNode → ViewportTextureNode → ViewportDepthTextureNode,即它本质上是TextureNode的一个特殊化:底层承载的纹理是DepthTexture(深度纹理)而不是普通的颜色纹理。
二、构造器与参数语义
源码(src/nodes/display/ViewportDepthTextureNode.js)给出的构造签名与文档一致:
constructor( uvNode = screenUV, levelNode = null, depthTexture = null )三个参数的含义与默认值整理如下:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
uvNode | Node | screenUV | 采样视口深度纹理时使用的 UV 坐标节点。默认screenUV即归一化屏幕坐标,范围[0, 1] |
levelNode | ?Node | null | 采样使用的 mip level(细节层级)。默认为null,表示自动或零级采样 |
depthTexture | ?DepthTexture | null | 自定义的深度纹理。若不提供,节点会使用一个共享的深度缓冲 |
其中screenUV来自 ScreenNode.js,是ScreenNode在UVscope 下的不可变单例,代表归一化屏幕坐标。把它作为默认 UV,意味着默认情况下“屏幕每个像素采样其自身位置的深度”。
共享深度缓冲机制
第三个参数depthTexture有一个值得注意的默认行为。构造器代码如下:
constructor( uvNode = screenUV, levelNode = null, depthTexture = null ) { if ( depthTexture === null ) { if ( _sharedDepthbuffer === null ) { _sharedDepthbuffer = new DepthTexture(); } depthTexture = _sharedDepthbuffer; } super( uvNode, levelNode, depthTexture ); }当不传入自定义深度纹理时,模块级私有变量_sharedDepthbuffer会以惰性方式创建一个全局共享的DepthTexture(首建后复用,见 src/nodes/display/ViewportDepthTextureNode.js)。这带来两个工程上的好处:
- 零配置开箱即用:直接
viewportDepthTexture()就能拿到视口深度,不需要先手动构造DepthTexture; - 避免资源浪费:所有未显式指定深度纹理的节点共享同一份缓冲,而不是每个材质都创建一份。
DepthTexture本身在 src/textures/DepthTexture.js 中定义,其默认参数值得注意:type默认UnsignedIntType、format默认DepthFormat,并且把flipY与generateMipmaps都强制覆盖为false(深度纹理按常规不做 Y 翻转、不生成 mipmap)。
三、TSL 便捷函数:viewportDepthTexture()
除了直接用类new ViewportDepthTextureNode(...)构造,模块底部还导出了一个 TSL 函数:
export const viewportDepthTexture = /*@__PURE__*/ nodeProxy( ViewportDepthTextureNode ).setParameterLength( 0, 3 );src/nodes/display/ViewportDepthTextureNode.js
该函数通过nodeProxy把构造器包装成语义化的 TSL 调用,且setParameterLength(0, 3)表示支持 0 到 3 个参数的多种调用形式:
viewportDepthTexture(); // 全部默认:screenUV + null + 共享深度缓冲 viewportDepthTexture( customUV ); // 自定义采样 UV viewportDepthTexture( uv, level ); // 自定义 UV + mip level viewportDepthTexture( uv, level, tex ); // 完全自定义在日常开发中几乎都使用该函数而非直接 new。它是 TSL 命名空间的一部分(本仓库中经由 src/Three.TSL.js 汇出),因此在使用 Node Material 的 WebGPU 示例里可以通过import { viewportDepthTexture } from 'three/tsl'引入。
四、底层原理:父类 ViewportTextureNode 如何工作
要真正理解深度纹理节点,需要看它的父类ViewportTextureNode(src/nodes/display/ViewportTextureNode.js)做了什么,因为深度节点自身只负责“换纹理类型”,采样与数据搬运逻辑全部继承自父类。
1. 通过“复制”而非“额外一遍渲染”取数
父类的设计核心是:从当前绑定的 framebuffer 里用复制操作抽取数据(见源码注释The module extracts data from the current bound framebuffer with a copy operation so no extra render pass is required)。相比“再渲染一遍场景到一张深度纹理”,这种做法省去了一整轮额外 pass,对性能友好。每帧更新由updateBeforeType = NodeUpdateType.RENDER驱动,即在每次渲染前执行updateBefore():
updateBefore( frame ) { // ... 根据渲染目标/画布计算实际尺寸 ... renderer.copyFramebufferToTexture( framebufferTexture ); }src/nodes/display/ViewportTextureNode.js
同时它会做“尺寸跟随”:比较 framebuffer 纹理的 image 宽高与当前绘制缓冲(drawing buffer)尺寸,不一致时更新并标记needsUpdate = true,从而保证纹理始终与视口分辨率一致。
2. 每个渲染上下文一份独立缓存(WeakMap)
为避免多渲染目标之间互相串用纹理导致渲染错误,父类用WeakMap<RenderTarget, FramebufferTexture>(_cacheTextures)按渲染目标/画布缓存各自的纹理,并提供getTextureForReference():
getTextureForReference( reference = null ) { // 若 reference 为 null(直接渲染到屏幕),返回 defaultFramebuffer // 否则在 cacheTextures 中按 reference 惰性 clone 一份并缓存 }src/nodes/display/ViewportTextureNode.js
而updateReference()决定当前“引用”是谁:
const renderTarget = renderer.getRenderTarget(); const canvasTarget = renderer.getCanvasTarget(); const reference = renderTarget ? renderTarget : canvasTarget;即:有离屏 RenderTarget 就按 RenderTarget 缓存纹理,否则按 CanvasTarget(默认画布)处理。这套机制是后续理解“何时该手动传深度纹理”的关键前提。
3. 从类层级到继承映射
由于ViewportDepthTextureNode extends ViewportTextureNode,父类自动new FramebufferTexture()的逻辑在这里被替换成了new DepthTexture()的共享缓冲逻辑(见第一节源码),而复制/缓存/尺寸同步/更新时机等行为被完整继承。类的继承映射对应如下:
| 层级 | 关键作用 | 默认纹理类型 |
|---|---|---|
TextureNode | 通用“采样式纹理节点”基类,接收 texture/uv/level | — |
ViewportTextureNode | 视口数据抽取:copy framebuffer、按引用缓存、尺寸同步、render 级更新 | FramebufferTexture |
ViewportDepthTextureNode | 把纹理类型固定为深度缓冲,并提供共享单例缓冲 | DepthTexture |
五、实战用法一:配合深度求值修正折射 UV(viewportSafeUV)
文档强调本节点“可与 viewport texture 组合实现需要深度求值的效果”。仓库中一个典型的组合范例是viewportSafeUV(src/nodes/utils/ViewportUtils.js)。
问题背景:用视口纹理做折射(refraction)时,若直接对screenUV做偏移去采样背景,位于折射面前方的物体也会被错误地“折射”显示到表面上。修复思路是用深度做一致性校验——先看偏移后 UV 处场景的深度,与折射表面自身深度是否一致,不一致就回退到未偏移的screenUV:
export const viewportSafeUV = /*@__PURE__*/ Fn( ( [ uv = null ] ) => { const depth = linearDepth(); const depthDiff = linearDepth( viewportDepthTexture( uv ) ).sub( depth ); const finalUV = depthDiff.lessThan( 0 ).select( screenUV, uv ); return finalUV; } );这里viewportDepthTexture( uv )采样的是偏移后 UV 处的场景深度,其结果是透视深度(perspective depth)。为了能与当前片元做线性比较,代码用linearDepth()把两侧都转换到线性深度空间后做差:depthDiff为负说明偏移点比表面更近(前方有物体挡住),此时退回screenUV。
依赖链完整路径为:
viewportDepthTexture(uv) // ViewportDepthTextureNode:采样视口深度纹理 → linearDepth(value) // ViewportDepthNode:透视深度→线性深度 → 与 linearDepth()(当前片元深度)做差比较其中linearDepth及perspectiveDepthToViewZ、viewZToOrthographicDepth等深度换算函数都在 src/nodes/display/ViewportDepthNode.js 中实现,且其内部已经正确处理 reversed depth buffer 的情况(见perspectiveDepthToViewZ对builder.renderer.reversedDepthBuffer的分支)。
该工具函数已实际用于示例中:
- examples/webgpu_refraction.html:
viewportSharedTexture( viewportSafeUV( refractorUV ) ),折射材质取安全 UV; - examples/webgpu_backdrop.html:对做了像素化/分格处理的 UV 再套一层
viewportSafeUV; - examples/jsm/objects/Water2Mesh.js:水面折射采样
viewportSharedTexture( viewportSafeUV( refractorUV ) )。
可见“视口深度纹理 + 深度换算 + UV 修正”是一条可复用的折射材质管线。
六、实战用法二:软粒子(SoftParticles)深度淡出
另一个官方配套实现位于 examples/jsm/tsl/utils/SoftParticles.js。softParticles()用深度纹理计算粒子与不透明场景之间的“缝隙”,当粒子贴近地面/墙面时按距离平滑淡出,避免生硬的裁剪边:
export function softParticles( { opacity = float( 1 ), distance = 1, contrast = 2, viewportDepth = viewportDepthTexture() } = {} ) { // 读取粒子 pass 之前捕获的不透明场景深度, // 并把场景深度与粒子自身深度都换算到 view space, // 从而能用世界单位度量两者之间的缝隙。 const sceneViewZ = perspectiveDepthToViewZ( viewportDepth, cameraNear, cameraFar ).toConst(); const depthDelta = positionView.z.sub( sceneViewZ ).div( distance ).saturate(); return opacity.mul( contrastCurve( depthDelta, contrast ) ); }配置参数完整说明:
| 参数 | 类型 | 默认值 | 语义 |
|---|---|---|---|
opacity | Node<float> | float(1) | 粒子的基础透明度,软淡出结果与之相乘 |
distance | Node<float> | number | 1 | 粒子相对场景淡出的世界空间距离(越大过渡越柔和) |
contrast | Node<float> | number | 2 | 淡出曲线的对比度幂次;1为线性,越大过渡越锐利 |
viewportDepth | Node | viewportDepthTexture() | 粒子所淡出到的不透明场景深度(默认即本主题的视口深度纹理节点) |
注意这里的默认值viewportDepth = viewportDepthTexture()正是第五节“共享深度缓冲”的直接受益者——用户无需关心深度缓冲从哪来,传粒子材质即可用。使用方式是把返回值接到material.opacityNode。该实现基于 NVIDIA “Soft Particles” 白皮书(Tristan Lorach)中描述的中心对称对比度曲线(contrastCurve)。
七、手动传入自定义 DepthTexture 的场景
构造函数允许第三个参数传入自定义DepthTexture。这在什么时候有必要?结合父类的缓存机制可以推断:
- 直接渲染到屏幕(单 RenderTarget):默认共享缓冲 + 父类
getTextureForReference(null)返回defaultFramebuffer的路径已足够,无需手动传入; - 多视图 / 多个离屏目标并存:
updateReference()会依据当前 RenderTarget 或 CanvasTarget 选择缓存纹理(不同引用得到不同缓存,见 ViewportTextureNode.js),此时如对每个上下文有特殊深度格式、比较函数(DepthTexture.compareFunction)等需求,可显式传入自定义纹理覆盖默认行为。
单元测试 test/unit/src/nodes/display/ViewportDepthTextureNode.tests.js 恰好验证了这套缓存契约,可以作为行为规范的“可执行文档”:
getTextureForReference(null)返回共享的defaultFramebuffer;- 不同 CanvasTarget 引用必须获得相互独立的缓存
DepthTexture实例(且都不等于共享缓冲); - 同一个引用重复获取必须返回同一份缓存纹理;
- 不同
RenderTarget(如new RenderTarget(512,512)与new RenderTarget(256,256))也各自独立缓存; - CanvasTarget 与 RenderTarget 之间互不复用缓存。
八、适用前提与边界
- 采样深度值的解释:直接采样得到的是透视深度 / 平台相关深度值,通常需要结合
cameraNear、cameraFar经 ViewportDepthNode.js 提供的perspectiveDepthToViewZ、linearDepth、viewZToOrthographicDepth等函数转换后才能用于线性比较或世界空间度量(前述两个实战示例都遵循该模式); - 该节点属于 TSL / Node Material 体系,从当前仓库结构看,它的直接调用方分布在 examples、examples/jsm 与 src/nodes 的 WebGPU 类示例中,均通过
three/tsl导入; - 自动深度管理:不传第三个参数即可获得共享深度缓冲,适合绝大多数“读一下场景深度”的场合;需要精细控制深度纹理格式或比较函数时再手动构造传入。
综上,ViewportDepthTextureNode是一个小却关键的“读视口深度”入口节点:向上它继承ViewportTextureNode的 framebuffer 复制与多上下文缓存能力,向下它把纹理类型锁定为共享DepthTexture,并以viewportDepthTexture()的 TSL 形式融入节点图。理解它之后,viewportSafeUV、softParticles以及任何需要“逐像素知道场景深度”的自定义效果都能顺理成章地搭建起来。
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考