three.js WebGPU 节点式像素化后期处理:PixelationPassNode 源码与实战完全指南
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
像素化(Pixelation)是复古游戏与像素艺术风格最常用的一种后期处理手法。在 three.js 的 WebGPU/TSL 渲染链路中,PixelationPassNode将传统"低分辨率渲染 + 颜色/法线/深度边缘检测"组合封装成一个可直接插入RenderPipeline的美感渲染通道(beauty pass)。本文以 PixelationPassNode 官方文档 为骨架,结合examples/jsm/tsl/display/PixelationPassNode.js源码与官方示例逐步剖析其继承关系、构造参数、公开属性/方法,并给出一份完整可运行的实战代码与参数调优建议。读完本文,你将能够在自己的 WebGPU 后处理管线中复现出带单像素描边的高质量像素化画面。
PixelationPassNode 是什么
PixelationPassNode是一个特殊的渲染通道节点(render pass node),它以低分辨率渲染场景,从而在视觉上产生"像素方块"效果,同时通过检测深度边缘与法线边缘,在色块交界处保留(或增强)描边细节,避免低分辨率采样把模型轮廓糊成一团。
它属于 three.js 的WebGPU/TSL(Three Shading Language)节点体系,其类层次为:
EventDispatcher → Node → TempNode → PassNode → PixelationPassNode其中父类PassNode的实现位于 src/nodes/display/PassNode.js,负责把场景与相机渲染到内部纹理(即所谓 beauty pass);PixelationPassNode则在其基础上改写渲染分辨率与采样方式,实现像素化输出。
与其功能相对应、但运行在传统 WebGL/EffectComposer链路中的同类组件是 examples/jsm/postprocessing/RenderPixelatedPass.js(非节点式 Pass),可作为迁移或对比参考。
导入方式
PixelationPassNode属于插件(addon)模块,不会随 three.js 核心自动打包,必须显式导入。推荐使用其附带的TSL 工厂函数pixelationPass:
import { pixelationPass } from 'three/addons/tsl/display/PixelationPassNode.js';若希望使用类本身做类型判断或继承扩展,也可以直接导入默认导出:
import PixelationPassNode from 'three/addons/tsl/display/PixelationPassNode.js';从源码可见该模块共导出了两个符号:PixelationPassNode.js 中pixelationPass只是对new PixelationPassNode(...)的语法糖封装,两者等价:
export const pixelationPass = ( scene, camera, pixelSize, normalEdgeStrength, depthEdgeStrength ) => new PixelationPassNode( scene, camera, pixelSize, normalEdgeStrength, depthEdgeStrength );构造函数与参数详解
new PixelationPassNode( scene, camera, pixelSize, normalEdgeStrength, depthEdgeStrength )源码中的完整签名与默认值(见 PixelationPassNode.js):
constructor( scene, camera, pixelSize = 6, normalEdgeStrength = 0.3, depthEdgeStrength = 0.4 )| 参数 | 类型 | 默认值 | 含义 |
|---|---|---|---|
scene | Scene | — | 要被渲染的场景 |
camera | Camera | — | 渲染场景所用的相机 |
pixelSize | Node<float>|number | 6 | 像素尺寸,即最终色块边长对应的"缩小倍数" |
normalEdgeStrength | Node<float>|number | 0.3 | 法线边缘强度 |
depthEdgeStrength | Node<float>|number | 0.4 | 深度边缘强度 |
参数解析要点:
scene与camera会被透传给父类PassNode(内部渲染目标的光圈被设为PassNode.COLOR)。- 三个数值参数不仅可以是普通数字,还可以是TSL 节点对象(如
uniform(...)包出的变量),从而支持运行时通过 GUI 或动画实时调节。官方示例 examples/webgpu_postprocessing_pixel.html 正是把三个参数全部做成uniform:effectController = { pixelSize: uniform( 6 ), normalEdgeStrength: uniform( 0.3 ), depthEdgeStrength: uniform( 0.4 ), pixelAlignedPanning: true };
公开属性
.pixelSize : number(默认6)
像素大小。除了在 shader 中控制色块粒度外,它还会影响内部渲染目标的分辨率(见下文setSize说明)。调大数值色块更大、画面更"糊"。
.normalEdgeStrength : number(默认0.3)
法线边缘强度。控制几何表面朝向突变处(如立方体的棱)边缘的检测力度与明暗对比。
.depthEdgeStrength : number(默认0.4)
深度边缘强度。控制前后景深度突变处(物体轮廓、穿插交界)边缘的强度。通常可略大于法线边缘强度以强化轮廓。
.isPixelationPassNode : boolean(只读,默认true)
用于类型检测的只读标志。在拿到不确定的节点对象时,可借助node.isPixelationPassNode === true判断其是否为像素化通道节点。
公开方法
.setSize( width : number, height : number )
设置通道(内部渲染目标)的尺寸。这是像素化效果的关键,见 PixelationPassNode.js:
setSize( width, height ) { const pixelSize = this.pixelSize.value ? this.pixelSize.value : this.pixelSize; const adjustedWidth = Math.floor( width / pixelSize ); const adjustedHeight = Math.floor( height / pixelSize ); super.setSize( adjustedWidth, adjustedHeight ); }也就是说,通道内部实际渲染分辨率会被压缩为⌊width / pixelSize⌋ × ⌊height / pixelSize⌋,画面先以小分辨率渲染、再被放大回目标尺寸,从而形成像素方块。注意这里兼容了"pixelSize是 uniform 节点"的情形:会优先读取this.pixelSize.value,因此运行时改大pixelSize也能让内部目标分辨率同步下降。
该方法重写了父类 PassNode#setSize。
.setup( builder : NodeBuilder ) : PixelationNode
节点构建方法,用于装配效果对应的 TSL 着色代码,返回一个内部的PixelationNode。其实现(见 PixelationPassNode.js)先从父类取出三张内部缓冲纹理,再送入像素化计算节点:
setup() { const color = super.getTextureNode( 'output' ); const depth = super.getTextureNode( 'depth' ); const normal = super.getTextureNode( 'normal' ); return pixelation( color, depth, normal, this.pixelSize, this.normalEdgeStrength, this.depthEdgeStrength ); }该方法重写了 PassNode#setup。
底层实现原理:低分辨率渲染 + 双通道边缘检测
要写出稳定的像素化效果,单纯降低分辨率是不够的——边缘检测必须拿到几何信息。PixelationPassNode的源码揭示了完整的实现链路。
1. 最近邻采样与 MRT 几何缓冲
构造函数中向父类传递了NearestFilter采样设置,并配置了一套MRT(多渲染目标):
super( PassNode.COLOR, scene, camera, { minFilter: NearestFilter, magFilter: NearestFilter } ); // ... this._mrt = mrt( { output: output, // 颜色输出 normal: normalView // 视角空间法线 } );最近邻过滤保证色块边缘锐利、无线性插值的模糊过渡;而 MRT 则在渲染场景的同时额外产出法线信息,供后续边缘检测使用(深度信息由渲染目标本身附带)。
2. 每帧刷新分辨率 uniform
内部的PixelationNode继承自TempNode,将其updateType设为NodeUpdateType.FRAME(每帧更新),并在update()回调中把输入纹理的宽高及倒数写入一个vec4uniform:
this._resolution.value.set( width, height, 1 / width, 1 / height );resolution.xy为全分辨率尺寸,resolution.zw则是单像素对应的归一化 UV 步长,供后续邻域采样使用(见 PixelationPassNode.js)。
3. 深度边缘检测(depthEdgeIndicator)
对当前像素的上、下、左、右四个邻域分别采样深度并计算差值,再经由smoothstep( 0.01, 0.02, ... )与floor(...).div(2)归一化为0 或 0.5两级深度边缘指示值——只有深度突变足够明显的像素才会被标记(源码 PixelationPassNode.js)。
4. 法线边缘检测(normalEdgeIndicator / neighborNormalEdgeIndicator)
法线边缘的判断要更精细,其规则可以从代码注释中读出:
- 只有**较浅(更靠近相机)**的一侧像素才被允许报告法线边缘,借助深度差值符号作为门控(
depthIndicator); - 用"邻域法线与当前法线越接近越趋近 1"的
normalIndicator避免同一面内部被误判; - 最终输出
1 - dot(normal, neighborNormal)与两个指示器相乘的累计值,并通过step( 0.1, indicator )二值化(源码 PixelationPassNode.js)。
从源码结构看,其中硬编码的normalEdgeBias = vec3(1,1,1)处还留有注释"This should probably be a parameter",说明法线偏置方向目前是固定值,未来存在参数化的可能。
5. 边缘与色块融合
像素化主函数Fn()内使用If按需开启各条分支——当某个强度参数为0时完全跳过对应的边缘计算,节省开销(源码 PixelationPassNode.js)。最终亮度修正强度为:
const strength = dei.greaterThan( 0 ) .select( float( 1.0 ).sub( dei.mul( this.depthEdgeStrength ) ), nei.mul( this.normalEdgeStrength ).add( 1 ) ); return vec4( texel.mul( strength ).rgb, texel.a );即深度边缘处的像素按depthEdgeStrength变暗形成轮廓;非法线/深度边缘的区域则整体提亮为1 + normalEdgeStrength,这正是"像素块 + 单像素描边"视觉风格的来源(见 PixelationPassNode.js)。
完整实战示例
官方演示页 examples/webgpu_postprocessing_pixel.html 是集成该节点的最小可运行样例,其完整使用方式如下。
步骤 1:配置 importmap
由于该示例同时使用 WebGPU 渲染器与 TSL 构建产物,需要把相关入口映射到build目录下的模块:
<script type="importmap"> { "imports": { "three": "../build/three.webgpu.js", "three/webgpu": "../build/three.webgpu.js", "three/tsl": "../build/three.tsl.js", "three/addons/": "./jsm/" } } </script>步骤 2:创建渲染管线并挂载节点
import * as THREE from 'three/webgpu'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { uniform } from 'three/tsl'; import { pixelationPass } from 'three/addons/tsl/display/PixelationPassNode.js'; renderer = new THREE.WebGPURenderer(); renderer.setSize( window.innerWidth, window.innerHeight ); renderer.setAnimationLoop( animate ); renderer.shadowMap.enabled = true; document.body.appendChild( renderer.domElement ); // 把三个效果参数做成可实时调用的 uniform const effectController = { pixelSize: uniform( 6 ), normalEdgeStrength: uniform( 0.3 ), depthEdgeStrength: uniform( 0.4 ), pixelAlignedPanning: true }; // 将像素化通道作为输出节点 const renderPipeline = new THREE.RenderPipeline( renderer ); const scenePass = pixelationPass( scene, camera, effectController.pixelSize, effectController.normalEdgeStrength, effectController.depthEdgeStrength ); renderPipeline.outputNode = scenePass;之后在动画循环中只需调用renderPipeline.render(),每帧都会先以低分辨率渲染场景,再套用边缘检测与色块放大。
步骤 3:用参数面板实时调参
官方示例借助WebGPURenderer.inspector暴露出三个核心参数(区间即官方示例推荐的调试范围):
const gui = renderer.inspector.createParameters( 'Settings' ); gui.add( effectController.pixelSize, 'value', 1, 16, 1 ).name( 'Pixel Size' ); gui.add( effectController.normalEdgeStrength, 'value', 0, 2, 0.05 ).name( 'Normal Edge Strength' ); gui.add( effectController.depthEdgeStrength, 'value', 0, 1, 0.05 ).name( 'Depth Edge Strength' ); gui.add( effectController, 'pixelAlignedPanning' );关于"像素对齐平移"(pixelAlignedPanning)
示例使用正交相机,并提供辅助函数pixelAlignFrustum():它会根据当前pixelSize计算出单块像素对应的世界单位,再微调相机平截体(frustum),使相机平移时色块始终与屏幕像素网格对齐,避免画面出现"滑动抖动"。只有当相机是正交相机时才需要这一辅助;透视相机下该技术不适用。
需要 WebGL 旧管线怎么办
如果你的项目仍运行在WebGLRenderer + EffectComposer上,可改用同主题的传统 Pass 类RenderPixelatedPass(见 RenderPixelatedPass.js):
const renderPixelatedPass = new RenderPixelatedPass( 6, scene, camera ); composer.addPass( renderPixelatedPass );其构造参数含义(pixelSize、以及通过options.normalEdgeStrength/options.depthEdgeStrength传入的边缘强度)与节点版一一对应,便于两个渲染后端之间迁移。
参数调优建议
综合源码逻辑与示例 GUI 区间,可按以下思路调试:
| 目标 | 建议做法 |
|---|---|
| 色块大小 | 调节pixelSize(示例区间1 ~ 16,步长 1)。1约等于无像素化,6左右是较自然的复古色块;过大时几何会丢失,需依赖更强的边缘描边兜底 |
| 轮廓更"黑"、更明显 | 增大depthEdgeStrength(示例区间0 ~ 1,步长 0.05)。只对深度突变处生效,不易误伤平滑表面 |
| 转折面棱线更清晰 | 增大normalEdgeStrength(示例区间0 ~ 2,步长 0.05)。对立方体棱、多面体转折效果显著 |
| 关闭某一类边缘 | 直接把对应强度设为0。源码中会跳过该分支的采样与计算,还能略微省下 GPU 开销 |
需要注意:pixelSize会同时改变内部渲染分辨率,因此该数值越小、渲染开销越高;场景较大或需要 60 FPS 时建议先用pixelSize = 8起步观察观感再回退。
小结
PixelationPassNode用约 330 行源码,把"低分辨率场景渲染 + MRT 几何缓冲 + 深度/法线邻域边缘检测 + 最近邻放大"这一套像素化方案完整封装成 WebGPU/TSL 渲染管线中即插即用的节点,并允许通过 TSL 节点参数实现运行时实时调节。它的直接使用入口是 TSL 工厂函数pixelationPass,内部逻辑可对照 examples/jsm/tsl/display/PixelationPassNode.js 阅读,运行效果可参考 webgpu_postprocessing_pixel.html。若需要更完整的后处理编排知识,可继续阅读 PostProcessing 与 RenderPipeline 文档,以及其父类 PassNode 的详细 API。
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考