首页
/ three.js TSL 后处理实践:DotScreenNode 网点(半调点阵)特效完整解析

three.js TSL 后处理实践:DotScreenNode 网点(半调点阵)特效完整解析

2026-09-06 18:22:10作者:秋泉律Samson

DotScreenNode 是 three.js 基于 TSL(Three Shading Language)节点系统实现的半调(Halftone)/ 网点屏幕后处理节点,用于把输入画面转换为由规则网格点阵构成的"点阵屏/丝网印刷"风格视觉。本文以 DotScreenNode.html.md 为核心,结合其 源码实现、GLSL 同源着色器 DotScreenShader 以及官方 WebGPU 后处理示例,讲解它的导入方式、构造参数、属性语义、底层算法与实战接入方案,让你能直接在三.js 的 WebGPU 渲染管线中复现并调优这一经典特效。

一句话读懂它

DotScreenNode 是一个继承自 TempNode 的后处理节点,作用是把输入的"场景画面"重新绘制成一幅由点阵网格构成的图片:画面越亮的位置点阵越密,越暗的位置点阵越疏(灰度信息以半调网点密度形式编码)。它常被串联在 pass( scene, camera ) 得到的场景通道之后,用于营造复古显示、报纸印刷、像素 CRT 等风格化效果。

导入方式:Addon 需显式引入

DotScreenNode 属于 three.js 的 addon 模块,不会被打进核心包,必须显式导入。官方文档约定的引入路径为:

import { dotScreen } from 'three/addons/tsl/display/DotScreenNode.js';

在源码中,DotScreenNode.js 的依赖同时也印证了它依赖的运行时环境:

import { TempNode } from 'three/webgpu';
import { nodeObject, Fn, uv, uniform, vec2, vec3, sin, cos, add, vec4, screenSize } from 'three/tsl';

也就是说,DotScreenNode 面向的是基于 three/webgpuWebGPURenderer)与 three/tsl 的节点化渲染体系,而不是传统 WebGLRenderer + ShaderPass 的管线。若你的工程已在 package.json 或 importmap 中把 three 指向 three.webgpu.js(如 webgpu_postprocessing.html 所示),即可直接使用。

提示:代码中同时导出了默认类 DotScreenNode 与 TSL 工厂函数 dotScreen。日常开发几乎只用小写驼峰形式的 dotScreen();只有需要自定义子类或直接实例化时才用 new DotScreenNode(...)

构造函数与参数语义

new DotScreenNode( inputNode : Node, angle : number, scale : number )

inputNode

代表特效输入画面的节点。实践中通常传入由 pass( scene, camera ) 生成的后处理场景通道节点,或另一个特效节点的输出,形成链式组合。在 webgpu_postprocessing.html 示例中,输入甚至可以是场景通道解出的颜色纹理节点:

const scenePass = pass( scene, camera );
const scenePassColor = scenePass.getTextureNode();
const dotScreenPass = dotScreen( scenePassColor );

angle

网点旋转角度(弧度),即点阵栅格相对画面坐标的倾斜角。默认值 1.57(约等于 π/2)。对照 GLSL 版着色器anglescale 的参数约定完全一致(GLSL 版同样默认 angle: 1.57scale: 1.0)。把它从默认值改为 0 会得到水平和垂直对齐的正交点阵,常用旋转值还能让点阵呈现更美观的对角线布局。

scale

特效缩放比例,值越大网点越小(越密),值越小网点越大(越疏)。默认值 1。这是调整视觉效果最直观的旋钮:

dotScreenPass.scale.value = 0.3;  // 放大网点、降低密度,详见官方示例

属性说明

.inputNode : Node

构造时传入的输入节点引用,setup() 中据此读取画面颜色。

.angle : UniformNode.

角度参数被封装为可实时更新的 uniform 节点(源码 DotScreenNode.js),类型为 UniformNode<float>。由于它是 UniformNode,想要在运行时动态改变旋转,需通过 .value 赋值:

dotScreenPass.angle.value += 0.01;   // 每帧旋转点阵,做出扫描动画

.scale : UniformNode.

同上,缩放被封装为 UniformNode<float>(源码 DotScreenNode.js),动态修改时同样走 .value

dotScreenPass.scale.value = 2.5;     // 值越大点越密

关键区分:new DotScreenNode(input, angle, scale) 传入的是普通数值,而实例暴露出的 .angle / .scale 却是 UniformNode——这正是 TSL 节点系统"构造时固化、渲染期可驱动"的参数设计。初始值经 uniform( angle ) 包装后写入属性。

底层实现:setup() 与算法拆解

.setup( builder : NodeBuilder ) : ShaderCallNodeInternal

setup() 用于生成该节点的 TSL 着色代码(重写自 TempNode#setup,参见 TempNode 文档)。它不接受 builder 之外的输入,内部完全是"纯函数式"的节点组合,最终返回一个类型为 ShaderCallNodeInternal 的调用节点。

算法分为两层函数(源码 DotScreenNode.js):

第一层 pattern()——生成网点模式

const s = sin( this.angle );
const c = cos( this.angle );

const tex = uv().mul( screenSize );                 // 像素级 UV 坐标
const point = vec2(
    c.mul( tex.x ).sub( s.mul( tex.y ) ),          // 旋转坐标 (x')
    s.mul( tex.x ).add( c.mul( tex.y ) )           // 旋转坐标 (y')
).mul( this.scale );                                // 缩放

return sin( point.x ).mul( sin( point.y ) ).mul( 4 );
  • uv()screenSize 相乘,把归一化 UV 换算成像素坐标;
  • 用 sin/cos 对坐标做二维旋转,把整个点阵栅格旋转 angle 弧度;
  • 对旋转后坐标乘 scale 后再取两个方向的 sin 乘积再乘 4,得到周期性明暗相间的网点亮度——两个正弦波相乘正是产生"规则网格点阵"的数学来源。

这段逻辑与十余年前就存在的 DotScreenShader 片元着色器中的 pattern() 几乎一一对应,差异仅在于:GLSL 版额外减去了 center(默认 (0.5, 0.5)),且乘法项写作 * 4.0;TSL 版以 mul( 4 ) 表达。整个算法源头可追溯到 glfx.js 的同名效果(见 DotScreenShader.js 的注释)。

第二层 dotScreen()——灰度到网点映射

const color = inputNode;
const average = add( color.r, color.g, color.b ).div( 3 );   // 简单灰度平均
return vec4( vec3( average.mul( 10 ).sub( 5 ).add( pattern() ) ), color.a );
  • 输入 RGB 先取均值得到灰度 average
  • average * 10 - 5 把灰度范围从 [0,1] 线性拉伸到 [-5,5];
  • 叠加上周期 [-4,4] 的网点模式后,经过 sin 裁切就自然形成"亮部出现点、暗部消失点"的半调效果;
  • alpha 通道原样透传。

GLSL 版对应写法(DotScreenShader.js)为:

vec4 color = texture2D( tDiffuse, vUv );
float average = ( color.r + color.g + color.b ) / 3.0;
gl_FragColor = vec4( vec3( average * 10.0 - 5.0 + pattern() ), color.a );

可以看出,TSL 版与 GLSL 版数学公式完全一致,但 TSL 版把"每像素采样、循环遍历"等 GPU 细节彻底封装进了节点组合里——开发者不再手写 uniform 声明和 main 函数,只需声明数据流向。

TSL 工厂函数 dotScreen()

模块底部导出了等价的 TSL 函数(DotScreenNode.js):

export const dotScreen = ( node, angle, scale ) =>
    new DotScreenNode( nodeObject( node ), angle, scale );

它内部自动对输入执行 nodeObject() 归一化(把普通对象/数值包装成合法节点),再交给类构造。这也是为什么导入时只需拿 dotScreen 直接用。

实战接入:在 WebGPURenderer 后处理链中使用

最小可运行骨架

WebGPURenderer 的后处理体系中,EffectComposer 已被 RenderPipeline 取代。DotScreenNode 的接入分为三步:

import * as THREE from 'three/webgpu';
import { pass } from 'three/tsl';
import { dotScreen } from 'three/addons/tsl/display/DotScreenNode.js';
import { rgbShift } from 'three/addons/tsl/display/RGBShiftNode.js';

// 1) 建立渲染管线
const renderPipeline = new THREE.RenderPipeline( renderer );

// 2) 先有场景通道,再挂网点特效
const scenePass = pass( scene, camera );
const dotScreenPass = dotScreen( scenePass );
dotScreenPass.scale.value = 1.0;

// 3) 把最终节点交给管线输出,动画循环中改用 renderPipeline.render()
renderPipeline.outputNode = dotScreenPass;

其中第 3 步用 renderPipeline.outputNode 替换整条渲染链的做法,与官方 WebGPU 后处理手册 一致;手册明确说明了使用 RenderPipeline 后动画循环应调用 renderPipeline.render() 而非 renderer.render( scene, camera )

与其它 TSL 特效节点自由串联

DotScreenNode 输出仍是普通 TSL 节点,因此可以继续作为下游节点的输入,形成"效果链"。官方 webgpu_postprocessing.html 正是这样把网点特效与 RGB 色差特效组合的:

renderPipeline = new THREE.RenderPipeline( renderer );

const scenePass = pass( scene, camera );
const scenePassColor = scenePass.getTextureNode().toInspector( 'Scene Color' );

const dotScreenPass = dotScreen( scenePassColor );
dotScreenPass.scale.value = 0.3;          // 拉低 scale → 更大的网点

const rgbShiftPass = rgbShift( dotScreenPass );
rgbShiftPass.amount.value = 0.001;        // 轻微色差

renderPipeline.outputNode = rgbShiftPass;

对应 WebGPU 后处理手册 对这套体系的设计说明:WebGPURenderer 自带完整 MRT 支持、系统会尽可能自动合并可组合的 effect 以减少渲染 pass 数量、整条效果链以"节点组合"表达——DotScreenNode 正是这套理念的直接受益者,串联多个特效时往往在单次 pass 内完成。

动态驱动的动画旋钮

由于 .angle.scale 是 UniformNode,在 renderer.setAnimationLoop( animate ) 的回调里按帧修改即可让网点旋转/缩放,形成动态扫描效果:

function animate() {
    dotScreenPass.angle.value += 0.005;   // 点阵缓慢旋转
    renderPipeline.render();
}

与 WebGL 传统方案的对照:DotScreenPass 与 DotScreenShader

如果读者正在维护 WebGLRenderer 旧项目,可在以下文件中找到同款效果的经典实现:

方案 文件 形态 适用渲染器
TSL 节点 examples/jsm/tsl/display/DotScreenNode.js DotScreenNode 类 + dotScreen() TSL 函数 WebGPURenderer + RenderPipeline
后处理 Pass examples/jsm/postprocessing/DotScreenPass.js 基于 ShaderPass 的封装 Pass WebGLRenderer + EffectComposer
原始 Shader examples/jsm/shaders/DotScreenShader.js ShaderMaterial 着色器对象 任意材质/Pass 底层

老版 DotScreenShader 还额外暴露了 tDiffuse(输入纹理)与 center(特效中心,默认 (0.5,0.5),以它为中点旋转缩放网点栅格)两个 uniform;新 TSL 版从接口上去掉了 center、统一以全屏像素坐标计算,并把 angle/scale 提升为一等公民的 UniformNode。从三种实现并存可以看出 three.js"同一算法、多代管线适配"的演进路径,若要在旧 WebGL 工程里先验证效果,可直接抄 GLSL 版。

源码阅读指引

想继续深挖这条技术路线,建议按以下顺序阅读仓库文件:

  1. 特效本体:examples/jsm/tsl/display/DotScreenNode.js——仅 100 余行,包含类定义、算法与 TSL 工厂函数;
  2. 运行时装配示例:examples/webgpu_postprocessing.html——展示 RenderPipeline + pass + dotScreen + rgbShift 的组合与参数驱动方式;
  3. 体系化手册:manual/pages/webgpu-postprocessing.html——说明 MRT、效果自动合并、outputNode 等设计动机;
  4. TSL 语言总览:docs/TSL.mddocs/pages/TSL.html.md
  5. 周边同类特效节点(同目录可横向对比 API 风格):RGBShiftNode、FXAANode、BloomNode、Lut3DNode 等均位于 examples/jsm/tsl/display/
  6. 传统 WebGL 对照:examples/jsm/shaders/DotScreenShader.jsexamples/jsm/postprocessing/DotScreenPass.js

小结

DotScreenNode 是理解 three.js TSL 后处理范式的一个绝佳切片:它在 100 行内完整演示了"TempNode 子类 + UniformNode 参数封装 + Fn 函数式着色 + TSL 工厂函数导出"的标准写法,而其数学内核又与 GLSL 时代完全同源。掌握它的构造、属性和 setup 逻辑后,你在阅读 examples/jsm/tsl/display 下的 Bloom、SSR、FXAA 等数十个特效节点时会感到高度一致,可直接把这套模式迁移到自己的自定义后处理特效开发中。

登录后查看全文
热门项目推荐
相关项目推荐