FEATURED · 精选文章

three.js WebGPU 光照节点阴影实现详解:ShadowNode 的 API、渲染流程与源码剖析

发布时间 / 2026/9/8 22:00:24
来源 / 创域科博编辑部
栏目 / 资讯中心
three.js WebGPU 光照节点阴影实现详解:ShadowNode 的 API、渲染流程与源码剖析 three.js WebGPU 光照节点阴影实现详解ShadowNode 的 API、渲染流程与源码剖析【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js本篇基于 three.js 仓库中的ShadowNode官方 API 文档系统讲解 WebGPU 渲染管线中默认的阴影实现节点ShadowNode如何被光照节点自动创建、它的属性与方法如何分工、阴影贴图shadow map从生成到采样过滤的完整调用链以及 Basic / PCF / VSM 三种过滤策略的源码级原理。读完后你可以独立配置 WebGPU 场景下的实时阴影、理解setup/setupShadow/updateBefore等生命周期方法的作用并能通过继承或light.shadow.shadowNode自定义阴影行为。1. ShadowNode 在节点光照体系中的位置ShadowNode是 three.js TSLThree Shading Language体系中光照节点AnalyticLightNode及其子类的默认阴影实现。继承链为EventDispatcher → Node → ShadowBaseNode → ShadowNode基类 ShadowBaseNode 封装了所有阴影节点的公共逻辑为每盏灯缓存一份专用的黑色NodeMaterial阴影通道材质、生成阴影通道渲染对象函数决定哪些对象参与阴影绘制并导出shadowPositionWorld这一 TSL 属性阴影通道的顶点世界坐标。ShadowNode 则负责平面/定向/聚光等类正交/类透视阴影贴图的具体实现而点光源使用其子类 PointShadowNode因为它需要立方体深度贴图与方向采样。TSL 侧提供了一等函数入口shadow( light, shadow )ShadowNode.js#L841它只是new ShadowNode( light, shadow )的语法糖并经由 src/nodes/TSL.js 对外导出。从源码结构看开发者通常不需要手动实例化ShadowNodeAnalyticLightNode在setupShadow时会自动创建。其默认实现为// src/nodes/lighting/AnalyticLightNode.js setupShadowNode() { return shadow( this.light ); }并允许通过light.shadow.shadowNode注入自定义节点AnalyticLightNode.js#L204-L234const customShadowNode this.light.shadow.shadowNode; let shadowNode; if ( customShadowNode ! undefined ) { shadowNode nodeObject( customShadowNode ); } else { shadowNode this.setupShadowNode(); } this.shadowNode shadowNode; this.shadowColorNode shadowColorNode this.colorNode.mul( shadowNode );也就是说光照颜色最终会乘以阴影节点输出的 [0,1] 遮罩这正是阴影作用于材质的原理。1.1 构造函数new ShadowNode( light : Light, shadow : LightShadow null )参数说明light投射阴影的光源shadow可选的LightShadow实例。默认null此时构造器回退为this.shadow shadow \|\| light.shadowShadowNode.js#L154构造函数还会初始化以下内部状态shadowMap初始null、四个 VSM 相关的渲染目标/材质字段初始null、私有字段_node缓存的输出节点、_currentShadowType当前阴影类型缓存、_cameraFrameId按相机去重阴影更新用的WeakMap、类型标志isShadowNode true以及depthLayer 0。2. 属性Properties逐项说明2.1 .depthLayer : number (readonly)当重写setupRenderTarget使用渲染目标数组array render target典型场景是点光源立方阴影时该索引用于指定采样数组中的深度层即.depth( depthLayer )。源码初始值为0ShadowNode.js#L244文档中标注默认true系 JSDoc 沿用写法实际以源码赋值为准。2.2 .isShadowNode : boolean (readonly)类型测试标志恒为true用于node.isShadowNode判断ShadowNode.js#L235。2.3 .shadow : LightShadow定义灯光阴影属性的LightShadow对象mapSize、bias、normalBias、radius、blurSamples、autoUpdate、needsUpdate等。默认null构造时若未显式传入则取light.shadow。2.4 .shadowMap : RenderTarget阴影贴图渲染目标的引用在setupShadow时通过setupRenderTarget创建并回写this.shadowMap shadowMap; this.shadow.map shadowMapShadowNode.js#L528-L529。默认null。setupRenderTarget的实现值得注意ShadowNode.js#L334-L347setupRenderTarget( shadow, builder ) { const depthTexture new DepthTexture( shadow.mapSize.width, shadow.mapSize.height ); depthTexture.name ShadowDepthTexture; depthTexture.compareFunction builder.renderer.reversedDepthBuffer ? GreaterEqualCompare : LessEqualCompare; const shadowMap builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height ); shadowMap.texture.name ShadowMap; shadowMap.texture.type shadow.mapType; shadowMap.depthTexture depthTexture; return { shadowMap, depthTexture }; }比较函数compareFunction随 WebGPU 的反向深度缓冲自动在GreaterEqualCompare/LessEqualCompare之间切换这是 WebGPU 管线与旧 WebGL 渲染器在阴影采样约定上的关键差异贴图类型跟随shadow.mapType。2.5 VSM 专属字段四个以下字段仅在renderer.shadowMap.type VSMShadowMap且光源非点光源时才会被创建属性类型作用vsmMaterialVerticalNodeMaterial渲染第一个 VSM pass 的节点材质vsmMaterialHorizontalNodeMaterial渲染第二个 VSM pass 的节点材质vsmShadowMapVerticalRenderTarget第一个 VSM pass 的输出渲染目标vsmShadowMapHorizontalRenderTarget第二个 VSM pass 的输出渲染目标默认均为null。创建逻辑见 ShadowNode.js#L383-L447两个中间渲染目标均为RGFormat HalfFloatTypeRG 通道分别存储均值 mean 与标准差 stddev且关闭深度缓冲当阴影相机已存在shadowMap._vsmShadowMapVertical/Horizental缓存时如shadowMap.depth 1的立方阴影直接复用避免重复分配。3. GPU 侧setup() 与阴影输出节点构建3.1 setup( builder ) : ShaderCallNodeInternal入口方法职责是在阴影贴图全局启用时构建最终输出节点setup( builder ) { if ( builder.renderer.shadowMap.enabled false ) return; return Fn( () { const currentShadowType builder.renderer.shadowMap.type; if ( this._currentShadowType ! currentShadowType ) { this._reset(); this._node null; } let node this._node; this.setupShadowPosition( builder ); if ( node null ) { this._node node this.setupShadow( builder ); this._currentShadowType currentShadowType; } if ( builder.material.receivedShadowNode ) { node builder.material.receivedShadowNode( node ); } return node; } )(); }ShadowNode.js#L595-L631要点renderer.shadowMap.enabled false时直接返回undefined即整条阴影链路不参与着色用_currentShadowType做类型级缓存切换shadowMap.type如从 PCF 切到 VSM时执行_reset()并重建输出节点先调用setupShadowPosition基类方法把material.receivedShadowPositionNode || context.shadowPositionWorld || positionWorld赋给shadowPositionWorld再构建阴影节点支持材质级钩子material.receivedShadowNode允许材质在默认阴影结果之上再加工例如接收阴影的强度曲线。3.2 setupShadow( builder ) : Node.真正的阴影输出节点工厂按顺序完成创建渲染目标setupRenderTarget见 2.4 节并根据类型配置过滤方式const shadowMapType renderer.shadowMap.type; const hasTextureCompare renderer.hasCompatibility( Compatibility.TEXTURE_COMPARE ); if ( shadowMapType PCFShadowMap hasTextureCompare ) { depthTexture.minFilter LinearFilter; depthTexture.magFilter LinearFilter; } else { depthTexture.minFilter NearestFilter; depthTexture.magFilter NearestFilter; }即仅当硬件支持比较采样且类型为 PCF 时才启用线性过滤硬件 PCF 每样本 4-tap否则用最近邻ShadowNode.js#L366-L376。VSM 分支为点光源以外的 VSM 阴影禁用硬件比较depthTexture.compareFunction null创建两个 RG/HalfFloat 中间目标并把 TSL 函数VSMPassVertical/VSMPassHorizontal分别挂到vsmMaterialVertical/vsmMaterialHorizontal的fragmentNode上输入通过reference绑定shadow.blurSamples、shadow.radius、shadow.mapSizeShadowNode.js#L383-L447。计算阴影坐标取shadow.bias与shadow.normalBias的reference构造阴影矩阵lightShadowMatrix( light )计算shadowPosition支持高精度的模型矩阵 uniform 优化路径见 ShadowNode.js#L457-L473。setupShadowCoord( builder, shadowPosition )把shadowPosition变换为采样坐标。核心逻辑ShadowNode.js#L280-L319正交阴影相机或无对数深度时shadowCoord.xyz / w透视除法透视阴影相机 对数深度缓冲时仅对 X/Y 做除法Z 分量用viewZToLogarithmicDepth( -w, near, far )计算且源码注释说明此处必须声明局部的near/far引用因为常规相机节点不会切换到阴影相机最终坐标按 WebGPU 规范翻转 YshadowCoord.y.oneMinus()Z 分量在reversedDepthBuffer时做z - bias否则z bias。setupShadowFilter( builder, inputs )执行视锥裁剪 过滤ShadowNode.js#L259-L271setupShadowFilter( builder, { filterFn, depthTexture, shadowCoord, shadow, depthLayer } ) { const frustumTest shadowCoord.x.greaterThanEqual( 0 ) .and( shadowCoord.x.lessThanEqual( 1 ) ) .and( shadowCoord.y.greaterThanEqual( 0 ) ) .and( shadowCoord.y.lessThanEqual( 1 ) ) .and( shadowCoord.z.lessThanEqual( 1 ) ); const shadowNode filterFn( { depthTexture, shadowCoord, shadow, depthLayer } ); return frustumTest.select( shadowNode, float( 1 ) ); }入参含义filterFn过滤类型函数如 PCF、depthTexture阴影深度贴图、shadowCoord采样坐标、shadow光源阴影、depthLayer数组贴图深度层。坐标落在[0,1]范围外或深度大于 1 时直接输出1不受光遮挡。组装最终输出过滤函数来自shadow.filterNode用户自定义或getShadowFilterFn( renderer.shadowMap.type )若解析为null会抛出THREE.WebGPURenderer: Shadow map type not supported yet.。若启用了renderer.shadowMap.transmitted透明物体透过率还会额外采样shadowMap.texture作为shadowColor。最终输出if ( shadowColor ) { shadowOutput mix( 1, shadowNode.rgb.mix( shadowColor, 1 ), shadowIntensity.mul( shadowColor.a ) ).toVar(); } else { shadowOutput mix( 1, shadowNode, shadowIntensity ).toVar(); }即以shadow.intensity在无阴影(1)与过滤结果之间插值。输出节点还会注册toInspector深度/颜色可视化便于调试时直接查看阴影贴图ShadowNode.js#L531-L584。3.3 getShadowFilterFn( type ) : function按阴影类型常量查表返回过滤函数。映射表在文件顶部定义ShadowNode.js#L117const _shadowFilterLib [ BasicShadowFilter, PCFShadowFilter, null /* PCFSoftShadowMap, removed */, VSMShadowFilter ];即对应BasicShadowMap、PCFShadowMap、已移除的PCFSoftShadowMap与VSMShadowMap四个常量索引。三个过滤函数全部用 TSL 写成节点函数位于 ShadowFilterNode.jsBasicShadowFilterL20-L32单次texture(depthTexture, shadowCoord.xy).compare( shadowCoord.z )二值[0,1]硬阴影PCFShadowFilterL48-L825 个 Vogel 圆盘采样点用 Interleaved Gradient NoiseIGN逐像素旋转采样盘以打散条带伪影配合硬件 PCF 的每样本 4-tap等效 20 个过滤采样。半径与贴图尺寸通过reference( radius/mapSize, shadow )实时绑定可动态调整VSMShadowFilterL93-L128采样 VSM 分布图RG均值/标准差用切比雪夫不等式给出片段深度小于均值概率的上界p_max variance / (variance d²)再 remap 到[0,1]以减轻光泄漏light bleeding最后与硬阴影取max。4. CPU 侧阴影贴图的生成与更新4.1 updateBefore( frame )按文档必要时执行阴影贴图的更新。基类将updateBeforeType覆写为NodeUpdateType.RENDER每渲染周期调用实现见 ShadowNode.js#L792-L826预编译阶段frame.renderer._isPreCompiling直接跳过不渲染阴影贴图触发条件为shadow.needsUpdate || shadow.autoUpdate用_cameraFrameIdWeakMapCamera, frameId做同一相机同帧只更新一次的去重更新后若深度贴图版本号未变会把shadow.needsUpdate复位为false避免下一帧重复渲染。4.2 updateShadow( frame )执行一次完整的阴影通道渲染流程ShadowNode.js#L665-L711缓存深度贴图版本号_depthVersionCached若阴影相机未设置独立 layer则临时借用主相机的 layer mask渲染后恢复resetRendererAndSceneState保存渲染器/场景状态随后scene.overrideMaterial this.getShadowMaterial()—— 基类缓存的黑色、NoBlending、fog false且带isShadowPassMaterial标志的NodeMaterialShadowBaseNode.js#L28-L47renderer.setRenderObjectFunction( this.getShadowRenderObjectFunction( ... ) )—— 阴影通道专用对象筛选函数renderer.setClearColor( 0x000000, 0 )、renderer.setRenderTarget( shadowMap )调用this.renderShadow( frame )若类型为 VSM 且非点光源阴影追加this.vsmPass( renderer )恢复 layer mask 与渲染器/场景状态。其中阴影通道的对象筛选规则ShadowBaseNode.js#L79-L120if ( object.castShadow true || ( object.receiveShadow shadowType VSMShadowMap ) ) { object.onBeforeShadow( renderer, object, _camera, shadow.camera, geometry, scene.overrideMaterial, group ); renderer.renderObject( object, scene, _camera, geometry, material, group, lightsNode, clippingContext, passId ); object.onAfterShadow( renderer, object, _camera, shadow.camera, geometry, scene.overrideMaterial, group ); }即普通阴影只渲染castShadow true的对象VSM 因为要记录接收面的深度分布receiveShadow的对象也必须参与。该函数按(renderer, shadow, shadowType, useVelocity)缓存避免每帧重建。4.3 renderShadow( frame )文档明确指出此逻辑本可并入updateShadow但拆出独立方法是为了让更专门的阴影节点可以重写默认行为。实现ShadowNode.js#L641-L658renderShadow( frame ) { const { shadow, shadowMap, light } this; const { renderer, scene } frame; shadow.updateMatrices( light ); shadowMap.setSize( shadow.mapSize.width, shadow.mapSize.height, shadowMap.depth ); const currentSceneName scene.name; scene.name Shadow Map [ ${ light.name || ID: light.id } ]; renderer.render( scene, shadow.camera ); scene.name currentSceneName; }要点shadow.updateMatrices( light )更新投影×视图矩阵与lightShadowMatrixsetSize支持运行时改mapSize渲染前临时把scene.name改为Shadow Map [ ... ]方便在 GPU 调试器中区分 pass渲染后还原。4.4 vsmPass( renderer )VSM 需要额外的两趟模糊 passShadowNode.js#L718-L734先按mapSize同步两个中间目标尺寸然后用共享的QuadMesh分别以vsmMaterialVertical垂直方向统计和vsmMaterialHorizontal水平方向统计渲染全屏四边形。两个 pass 的 TSL 实现VSMPassVertical/VSMPassHorizontalShadowNode.js#L37-L115在blurSamples个采样点上累加深度求均值mean与方差输出sqrt( squaredMean - mean² )作为标准差——这正是后续VSMShadowFilter中切比雪夫不等式所需的(mean, stddev)分布。5. 资源管理dispose() 与 _reset()dispose()ShadowNode.js#L739-L745调用私有_reset()后转发给ShadowBaseNode#dispose。_reset()ShadowNode.js#L752-L785释放的内容清除类型缓存_currentShadowTypedisposeShadowMaterial()销毁该灯对应的阴影通道材质shadowMap.dispose()并置空VSM 两个渲染目标与两个NodeMaterial分别 dispose 后置null。注意光照节点在光源dispose事件时会级联调用this.shadowNode.dispose()AnalyticLightNode.js#L99-L109、L128-L137因此移除光源即会回收其阴影资源若你自行创建了ShadowNode并挂到light.shadow.shadowNode上则需要在场景变更时显式调用dispose()。6. 实战配置与自定义6.1 基础配置WebGPU 渲染器import * as THREE from three/webgpu; const renderer new THREE.WebGPURenderer(); // 全局开关ShadowNode.setup() 在 enabled false 时直接不产出输出节点 renderer.shadowMap.enabled true; // BasicShadowMap / PCFShadowMap / VSMShadowMap切换时 ShadowNode 会自动 _reset 重建 renderer.shadowMap.type THREE.PCFShadowMap; const dirLight new THREE.DirectionalLight( 0xffffff, 3 ); dirLight.position.set( 5, 10, 7 ); // 以下属性与 ShadowNode 的采样参数一一对应 dirLight.shadow.mapSize.set( 2048, 2048 ); // 渲染目标尺寸setupRenderTarget / updateShadow 使用 dirLight.shadow.camera.near 0.1; // 透视深度对数分支的 cameraFarLocal/cameraNearLocal dirLight.shadow.camera.far 100; dirLight.shadow.camera.left - 17; dirLight.shadow.camera.right 17; dirLight.shadow.camera.top 17; dirLight.shadow.camera.bottom - 17; dirLight.shadow.bias - 0.0001; // setupShadowCoord 的 z 分量偏置 dirLight.shadow.normalBias 0.02; // 沿法线方向偏移抑制自遮挡acne dirLight.shadow.radius 4; // PCF 采样半径像素 dirLight.shadow.blurSamples 8; // VSM 模糊 pass 的采样数 scene.add( dirLight );上述参数命名与 examples/webgpu_shadowmap.html 官方示例一致该示例中同样配置了shadow.camera.*、mapSize与radius并设置renderer.shadowMap.enabled true。6.2 自定义阴影节点由于AnalyticLightNode#setupShadow优先读取light.shadow.shadowNode可以注入自定义实现通常是PointShadowNode之类的子类覆写setupShadow/renderShadowimport { ShadowNode } from three/webgpu; const spotLight new THREE.SpotLight( 0xffffff, 50 ); spotLight.shadow.mapSize.set( 1024, 1024 ); // 显式构造并挂接替代默认的 shadow( this.light ) const custom new ShadowNode( spotLight ); // 第二个参数可传入独立 LightShadow spotLight.shadow.shadowNode custom; // 使用完毕、更换灯光前 // custom.dispose();也可以只替换过滤策略给shadow.filterNode赋一个 TSLFn在setupShadow中const filterFn shadow.filterNode || this.getShadowFilterFn( renderer.shadowMap.type )从而不改类而换采样算法。7. 方法速查表方法时机 / 调用方作用setup( builder )材质构建期renderer.shadowMap.enabled为真时构建并缓存阴影输出节点支持receivedShadowNode钩子setupShadow( builder )setup内部创建渲染目标/过滤配置/坐标/输出节点setupShadowCoord( builder, shadowPosition )setupShadow内部透视除法、对数深度、bias 与 Y 翻转setupShadowFilter( builder, inputs )setupShadow内部视锥测试 调用过滤函数getShadowFilterFn( type )setupShadow内部按类型查过滤函数表updateBefore( frame )每渲染周期判断并触发阴影贴图更新含同帧去重updateShadow( frame )updateBefore内部组织一次完整阴影通道渲染renderShadow( frame )updateShadow内部可重写的阴影贴图渲染入口vsmPass( renderer )updateShadow末尾VSM 时垂直/水平两趟 VSM 模糊dispose()光源销毁或手动调用释放渲染目标、材质与阴影材质8. 小结与适用前提ShadowNode把阴影贴图生成CPU 驱动的updateBefore → updateShadow → renderShadow与阴影采样过滤GPU 侧setup → setupShadow → setupShadowCoord/Filter拆分为两条清晰的路径且每一环节都可以被子类重写——PointShadowNode继承它实现立方阴影即为范例PointLightNode.js#L83 的setupShadowNode返回pointShadow( this.light )适用前提本实现运行于 WebGPU 渲染管线three/webgpu入口节点体系见 src/nodes/Nodes.js深度比较与 Y 坐标约定均按 WebGPU 规范处理reversedDepthBuffer、y.oneMinus()三个可调档位BasicShadowMap硬阴影、最快、PCFShadowMapVogel 圆盘 IGN需硬件比较采样支持才有硬件 4-tap、VSMShadowMap可运行时调radius/blurSamples但点光源阴影不走 VSM 分支且接收面也要参与阴影通道渲染调试技巧setupShadow返回的节点自带toInspector深度与颜色视图配合 devtools 面板 可直接观察Shadow Map [ 灯名 ]场景的渲染结果。【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻