
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是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 如何工作要真正理解深度纹理节点需要看它的父类ViewportTextureNodesrc/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为避免多渲染目标之间互相串用纹理导致渲染错误父类用WeakMapRenderTarget, 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 级更新FramebufferTextureViewportDepthTextureNode把纹理类型固定为深度缓冲并提供共享单例缓冲DepthTexture五、实战用法一配合深度求值修正折射 UVviewportSafeUV文档强调本节点“可与 viewport texture 组合实现需要深度求值的效果”。仓库中一个典型的组合范例是viewportSafeUVsrc/nodes/utils/ViewportUtils.js。问题背景用视口纹理做折射refraction时若直接对screenUV做偏移去采样背景位于折射面前方的物体也会被错误地“折射”显示到表面上。修复思路是用深度做一致性校验——先看偏移后 UV 处场景的深度与折射表面自身深度是否一致不一致就回退到未偏移的screenUVexport 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.htmlviewportSharedTexture( viewportSafeUV( refractorUV ) )折射材质取安全 UVexamples/webgpu_backdrop.html对做了像素化/分格处理的 UV 再套一层viewportSafeUVexamples/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 ) ); }配置参数完整说明参数类型默认值语义opacityNodefloatfloat(1)粒子的基础透明度软淡出结果与之相乘distanceNodefloat | number1粒子相对场景淡出的世界空间距离越大过渡越柔和contrastNodefloat | 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 之间互不复用缓存。八、适用前提与边界采样深度值的解释直接采样得到的是透视深度 / 平台相关深度值通常需要结合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),仅供参考