news 2026/9/9 20:39:05

three.js 节点材质中的视口深度纹理节点:ViewportDepthTextureNode 原理与实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
three.js 节点材质中的视口深度纹理节点:ViewportDepthTextureNode 原理与实战指南

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 )

三个参数的含义与默认值整理如下:

参数类型默认值说明
uvNodeNodescreenUV采样视口深度纹理时使用的 UV 坐标节点。默认screenUV即归一化屏幕坐标,范围[0, 1]
levelNode?Nodenull采样使用的 mip level(细节层级)。默认为null,表示自动或零级采样
depthTexture?DepthTexturenull自定义的深度纹理。若不提供,节点会使用一个共享的深度缓冲

其中screenUV来自 ScreenNode.js,是ScreenNodeUVscope 下的不可变单例,代表归一化屏幕坐标。把它作为默认 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)。这带来两个工程上的好处:

  1. 零配置开箱即用:直接viewportDepthTexture()就能拿到视口深度,不需要先手动构造DepthTexture
  2. 避免资源浪费:所有未显式指定深度纹理的节点共享同一份缓冲,而不是每个材质都创建一份。

DepthTexture本身在 src/textures/DepthTexture.js 中定义,其默认参数值得注意:type默认UnsignedIntTypeformat默认DepthFormat,并且把flipYgenerateMipmaps都强制覆盖为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()(当前片元深度)做差比较

其中linearDepthperspectiveDepthToViewZviewZToOrthographicDepth等深度换算函数都在 src/nodes/display/ViewportDepthNode.js 中实现,且其内部已经正确处理 reversed depth buffer 的情况(见perspectiveDepthToViewZbuilder.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 ) ); }

配置参数完整说明:

参数类型默认值语义
opacityNode<float>float(1)粒子的基础透明度,软淡出结果与之相乘
distanceNode<float> | number1粒子相对场景淡出的世界空间距离(越大过渡越柔和)
contrastNode<float> | number2淡出曲线的对比度幂次;1为线性,越大过渡越锐利
viewportDepthNodeviewportDepthTexture()粒子所淡出到的不透明场景深度(默认即本主题的视口深度纹理节点)

注意这里的默认值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 之间互不复用缓存。

八、适用前提与边界

  • 采样深度值的解释:直接采样得到的是透视深度 / 平台相关深度值,通常需要结合cameraNearcameraFar经 ViewportDepthNode.js 提供的perspectiveDepthToViewZlinearDepthviewZToOrthographicDepth等函数转换后才能用于线性比较或世界空间度量(前述两个实战示例都遵循该模式);
  • 该节点属于 TSL / Node Material 体系,从当前仓库结构看,它的直接调用方分布在 examples、examples/jsm 与 src/nodes 的 WebGPU 类示例中,均通过three/tsl导入;
  • 自动深度管理:不传第三个参数即可获得共享深度缓冲,适合绝大多数“读一下场景深度”的场合;需要精细控制深度纹理格式或比较函数时再手动构造传入。

综上,ViewportDepthTextureNode是一个小却关键的“读视口深度”入口节点:向上它继承ViewportTextureNode的 framebuffer 复制与多上下文缓存能力,向下它把纹理类型锁定为共享DepthTexture,并以viewportDepthTexture()的 TSL 形式融入节点图。理解它之后,viewportSafeUVsoftParticles以及任何需要“逐像素知道场景深度”的自定义效果都能顺理成章地搭建起来。

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 20:34:45

Deepcoin赞助阿根廷足协:区域赞助如何撬动Web3品牌出海

Deepcoin官宣成为阿根廷足协&#xff08;AFA&#xff09;官方区域赞助商&#xff0c;消息出来当天&#xff0c;我身边几个做品牌和加密市场的朋友就开始讨论。大家讨论的点很一致&#xff1a;一个数字资产交易平台&#xff0c;不去硬砸世界杯全球赞助&#xff0c;偏偏选择AFA的…

作者头像 李华
网站建设 2026/9/9 20:32:55

K2算法详解:从贝叶斯网络结构学习到Python实现

简介&#xff1a;K2算法是贝叶斯网络结构学习中的经典贪心搜索方法&#xff0c;这份资源提供了利用K2算法从数据中学习贝叶斯网络结构的完整MATLAB实现&#xff0c;面向机器学习、数据挖掘方向的研究者与学生&#xff0c;适合需要理解结构学习原理或在项目中快速搭建K2模块的读…

作者头像 李华
网站建设 2026/9/9 20:32:31

图论基础习题集锦:从图的存储到遍历、最短路与拓扑排序

平时帮人复盘图论基础题错因&#xff0c;我发现一个特别普遍的现象&#xff1a;很多人不是不会写代码&#xff0c;而是面对题目里的图时&#xff0c;脑子里没有一条清晰的“识别路径”。看到题目第一反应永远是“这题该用哪个算法”&#xff0c;而不是先问自己“这张图长什么样…

作者头像 李华