three.js TSL 中的 SSSNode 完全解析用屏幕空间阴影Contact Shadows为场景补上细节阴影【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.jsthree.js 的 TSLThree Shading Language体系下SSSNode 是一个专门用于屏幕空间阴影Screen-Space Shadows简称 SSS又称 Contact Shadows的后处理节点。它通过深度反投影加光线步进ray marching的方式在主光源方向上计算物体之间近距离、高精度的接触阴影与传统的 Shadow Map 形成互补Shadow Map 负责基础SSS 负责细节。本文基于官方文档 SSSNode 页面 与仓库中 SSSNode 源码、官方示例完整梳理其 API、参数、底层实现与接入方式。1. 定位与适用场景Shadow Map 打底SSS 补细节官方文档明确给出 SSSNode 的定位Screen-Space Shadows也叫 Contact Shadows理想上应该作为传统 Shadow Map 的补充。它们最适合渲染小尺度、近距离物体的高精度阴影比如高细节模型上复杂的阴影结构。换言之用 Shadow Map 打基础用 Screen-Space Shadows 补细节。这个定位在源码中得到了印证。SSSNode.js 的类注释与文档完全一致并补充了三点重要限制阴影长度不要超过 1 米。超过这个长度后光线步进过程需要评估的采样数会显著增加计算代价急剧上升可以通过调低quality参数缓解。只能配合单个平行光DirectionalLight使用即场景的主光源通常代表太阳或日光。和其他 Screen-Space 技术一样SSS 只能利用当前相机视野内可见的物体参与阴影计算视野外的遮挡物不会生效。因此SSSNode 的典型场景是一个带主平行光、开启了shadowMap的场景其中模型细节丰富如官方示例使用的 Nemetona_NatureBeauty 高模需要比 1024×1024 阴影贴图更细腻的接触阴影。2. 导入方式作为 addon 显式引入SSSNode 属于 addon必须显式导入安装方式参考 three.js 官方 manual 的 Addons 一节import { sss } from three/addons/tsl/display/SSSNode.js;在源码 SSSNode.js 末尾sss是一个 TSL 函数等价于直接构造节点export const sss ( depthNode, camera, mainLight ) new SSSNode( depthNode, camera, mainLight );文件内部依赖three/webgpuRenderTarget、QuadMesh、NodeMaterial、NodeUpdateType 等和three/tslFn、Loop、passTexture、interleavedGradientNoise等函数这说明它运行在 WebGPU 渲染管线WebGPURendererRenderPipeline之下。3. 构造函数与全部属性3.1 构造函数new SSSNode( depthNode : TextureNode, camera : Camera, mainLight : DirectionalLight )参数类型说明depthNodeTextureNode表示场景深度的纹理节点通常来自 pre-pass 的 depth 纹理cameraCamera渲染场景所用的相机透视或正交均可源码会分别处理mainLightDirectionalLight场景的主平行光3.2 属性一览以下属性默认值均与 SSSNode.js 构造函数 中的实际初始化代码一一对应属性类型默认值说明.depthNodeTextureNode—beauty pass 的深度节点.maxDistanceUniformNode.float0.1阴影最大长度世界单位。值越大阴影越长但光线步进开销越大.qualityUniformNode.float0.5控制光线步进精度取值[0, 1]1为最高质量最多迭代/采样数0为不采样。0.5对多数场景够用应尽可能调低.resolutionScalenumber1分辨率缩放取值[0, 1]。0.5表示以半分辨率计算效果显著降低开销.shadowIntensityUniformNode.float1.0阴影强度必须位于[0, 1].thicknessUniformNode.float0.01深度测试的厚度决定射线与表面贴面判定的容差.updateBeforeTypestringframe设为NodeUpdateType.FRAME因为节点在updateBefore()中每帧渲染一次自己的效果.useTemporalFilteringbooleanfalse是否启用时间滤波。设为true要求使用TRAANode能减少噪声但会引入 TAA 典型的残影ghosting与时间不稳定伪影3.3 方法一览方法说明.dispose()释放内部资源渲染目标与材质不再需要效果时调用.getTextureNode() : PassTextureNode以纹理节点形式返回效果结果供后续合成.setSize( width, height )设置效果尺寸内部会按resolutionScale缩放.setup( builder : NodeBuilder ) : PassTextureNode搭建效果的 TSL 代码每帧管线构建时调用.updateBefore( frame : NodeFrame )每帧渲染一次 SSS 全屏 pass4. 最小可用示例创建 SSS pass 并可选模糊官方文档给出的最小代码const sssPass sss( scenePassDepth, camera, mainLight ); const sssBlur boxBlur( sssPass.r, { size: 2, separation: 1 } ); // optional blur文档特别提醒SSS 产生的阴影边缘可能过硬建议在与 beauty pass 合成前用 box、gaussian 或 hash blur 柔化。仓库中 boxBlur 的注释给出了使用要点kernelsize建议取 1~3采样数为(size*21)^2若需要更宽的模糊半径应增大separation而不是size因为separation不影响性能。5. 完整实战示例RenderPipeline Pre-Pass TAA官方示例 webgpu_postprocessing_sss.html 演示了 SSS 与 Shadow Map、TRAANode 的完整组合其关键接线如下已按源码实际内容整理import * as THREE from three/webgpu; import { pass, vec3, vec4, mrt, screenUV, velocity, builtinShadowContext } from three/tsl; import { sss } from three/addons/tsl/display/SSSNode.js; import { traa } from three/addons/tsl/display/TRAANode.js; renderer new THREE.WebGPURenderer(); renderer.shadowMap.enabled true; // 主平行光SSS 只能配合它工作 const dirLight new THREE.DirectionalLight( 0xffffff, 3 ); dirLight.position.set( - 3, 10, - 10 ); dirLight.castShadow true; dirLight.shadow.bias - 0.001; // 消除自阴影伪影 dirLight.shadow.mapSize.width 1024; dirLight.shadow.mapSize.height 1024; const renderPipeline new THREE.RenderPipeline( renderer ); // pre-pass拿到 depth 与 velocityvelocity 供 TAA 使用 const prePass pass( scene, camera ); prePass.transparent false; prePass.setMRT( mrt( { output: velocity } ) ); const prePassDepth prePass.getTextureNode( depth ).toLinearDepth(); // scene passbeauty pass const scenePass pass( scene, camera ); // SSS 接入 const sssPass sss( prePassDepth, camera, dirLight ); sssPass.maxDistance.value 0.2; // 例中用 0.2 米 sssPass.useTemporalFiltering true; // 启用时间滤波配合 TAA // 将 SSS 结果注入场景光照上下文 const sssSample sssPass.getTextureNode().sample( screenUV ).r; const sssContext builtinShadowContext( sssSample, dirLight ); scenePass.contextNode sssContext; // 输出启用时间滤波时接 TAA否则直接输出 scenePass const traaPass traa( scenePass, prePassDepth, prePassVelocity, camera ); renderPipeline.outputNode sssPass.useTemporalFiltering ? traaPass : scenePass;示例中的 Inspector GUI 还暴露了四个可实时调节的参数可作为调参起点gui.add( sssPass.shadowIntensity, value, 0, 1 ).name( shadow intensity ); gui.add( sssPass.maxDistance, value, 0.01, 1 ).name( max ray distance ); gui.add( sssPass.quality, value, 0, 1 ).name( quality ); gui.add( sssPass.thickness, value, 0.01, 0.1 ).name( thickness );GUI 给出的取值范围maxDistance0.01~1、thickness0.01~0.1与文档最大阴影长度不超过 1 米的限制相吻合。示例还提供了三档输出对比Shadow Maps SSS / 仅 Shadow Maps / 纯 SSS 结果便于直观验证 SSS 的增量贡献。其中builtinShadowContext定义在 ContextNode.js其实现为对指定light将阴影颜色与 SSS 采样结果相乘shadowColorNode.mul( shadowNode )从而让场景光照在该光源上减去屏幕空间阴影——这也是 SSS 结果进入最终画面的合成路径。6. 源码原理光线步进是怎么跑的SSSNode.js 的setup()构建了整个 TSL 着色逻辑流程如下与文档各参数一一呼应深度采样与相机适配。sampleDepth()从depthNode采样深度若渲染器开启了logarithmicDepthBuffer则先做logarithmicDepthToViewZ再转回透视深度getViewZ()根据相机类型分别走perspectiveDepthToViewZ或orthographicDepthToViewZ。这也解释了构造函数中为何用reference缓存相机的near/far。构建光线view-space。用getViewPosition由 uv depth 反投影出光线起点rayStartPosition方向为相机视图矩阵 × (lightPosition - lightTargetPosition)即主平行光方向rayEndPosition rayStartPosition rayDirection * maxDistance——maxDistance在这里被逐像素乘进射线长度因此它是控制开销的第一杠杆。屏幕空间步进规划。把射线首尾投影到屏幕坐标得到d0、d1总步数totalStep int( max(|xLen|, |yLen|) * quality.clamp() )——quality直接缩放迭代次数与文档quality 越高开销越大的描述一致xSpan/ySpan为每步在 X/Y 方向的位移。噪声抖动。offset由interleavedGradientNoise(screenCoordinate)加上_temporalOffset与rand(uv frameId)构成。_spatialOffsets [0, 0.5, 0.25, 0.75]在 updateBefore() 中按frameId % 4轮转写入_temporalOffset——这正是useTemporalFiltering的实现四帧一组空间抖动等价于 4 倍超采样代价是时间相关性伪影所以文档要求配合 TAA。步进与遮挡判定。Loop(totalStep)中每帧推进屏幕坐标越界超出[0, resolution]立即Break否则采样该位置的深度并转 viewZ插值出射线当前深度rayPosition.z计算depthDelta rayPosition.z - fragmentViewZthree 中 viewZ 为负代码注释特别标注了这一点。当0 depthDelta thickness时判定为遮挡——射线刚好压在表面背后一个小厚度内——立即把occlusion置为shadowIntensity并终止循环。最终输出occlusion.oneMinus()1 为完全被照亮被遮挡处趋向1 - shadowIntensity。其他实现细节输出写入一个独立的单通道渲染目标new RenderTarget(1, 1, { depthBuffer: false, format: RedFormat, type: UnsignedByteType })因此结果纹理只读红通道示例中sssSample ...sample(screenUV).r且默认清屏色为白色全照亮。setSize()按resolutionScale缩放渲染目标尺寸即resolutionScale 0.5时 SSS 以半分辨率计算与文档描述一致。生命周期管理由TempNode基类TempNode.js提供updateBeforeType NodeUpdateType.FRAME保证updateBefore()每帧执行一次全屏 passdispose()会释放渲染目标与材质。注意源码中occlusion一旦被置为shadowIntensity就不再衰减输出是硬的 0/1 形态——这正是文档建议使用 blur 柔化边缘的原因。7. 参数调优建议基于文档与示例取值结合文档说明、SSSNode.js 默认值 和官方示例 GUI 的取值范围实际使用中可参考maxDistance默认0.1米偏保守示例用了0.2。不要超过 1 米否则步进采样数暴涨。quality默认0.5文档建议尽量调低移动端或性能敏感场景可再降。thickness默认0.01示例调参范围0.01~0.1。它控制射线判定的贴面容差过小可能漏判遮挡过大可能在近表面处误伤产生过早的阴影。resolutionScale默认1在画质允许时设为0.5是开销减半的低成本手段。useTemporalFiltering需要配合traa()使用开启后renderPipeline.outputNode应接 TAA pass关闭时可直接输出 scenePass示例中用updatePostprocessing()动态切换两者并置renderPipeline.needsUpdate true。边缘柔化合成前接boxBlur低成本或 gaussian/hash blurboxBlur的size保持 1~3、用separation控制半径。8. 相关资源文档源SSSNode 文档页实现examples/jsm/tsl/display/SSSNode.js官方示例Shadow Map SSS 组合、含 GUI 调参examples/webgpu_postprocessing_sss.html时间滤波配套examples/jsm/tsl/display/TRAANode.js、边缘柔化examples/jsm/tsl/display/boxBlur.jsSSS 结果注入光照的机制src/nodes/core/ContextNode.js参考论文/文章文档给出的 ReferencesPanos Karabelas 的Screen Space Shadows系列与 Bend Studio 的Inside Bends Screen Space Shadows实现中的噪声抖动、贴面厚度判定等手法即源自这些工作。【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考