首页
/ three.js AnaglyphPassNode 深度解析:基于物理正确离轴立体投影的红青色差立体渲染(TSL/WebGPU 版)

three.js AnaglyphPassNode 深度解析:基于物理正确离轴立体投影的红青色差立体渲染(TSL/WebGPU 版)

2026-09-06 23:40:14作者:傅爽业Veleda

本文围绕 three.js 官方 API 文档 AnaglyphPassNode 展开,讲解这个 TSL 渲染 Pass 节点如何把左右眼视图合成为红青色差(Anaglyph)立体图像。文档将完整覆盖其构造函数、eyeSep / planeDistance / algorithm / colorMode 等全部属性与默认值,并结合仓库源码 examples/jsm/tsl/display/AnaglyphPassNode.js 剖析其离轴投影的数学实现与色差矩阵体系,最后给出官方示例中的完整可运行用法。读完后你将掌握:如何在 WebGPU 渲染管线中接入 Anaglyph 效果、如何通过 frameCorners() 实现零视差的虚拟屏幕平面,以及 7 种合成算法 × 3 种色卡的矩阵是如何定义与切换的。

1. 定位与继承体系

AnaglyphPassNode 是一个"渲染 Pass 节点",用物理正确的离轴(off-axis)立体投影生成红青色差立体效果。其官方描述为:

A render pass node that creates anaglyph effect using physically-correct off-axis stereo projection. This implementation uses CameraUtils.frameCorners() to align stereo camera frustums to a virtual screen plane, providing accurate depth perception with zero parallax at the plane distance.

即:它借助 CameraUtils.frameCorners() 把立体相机的视锥体对齐到一个虚拟屏幕平面,在该平面的距离处视差为零,从而提供准确的深度感知。

继承链(来自文档首行):

EventDispatcher → Node → TempNode → PassNode → StereoCompositePassNode → AnaglyphPassNode

从源码结构看,AnaglyphPassNode 继承自 StereoCompositePassNode,后者是一个抽象的"立体复合 Pass":与普通 StereoPassNode 输出左右两路视图不同,它把左右眼图像合并为单张图像——这正是色差合成或视差屏障(Parallax Barrier)类效果所必需的能力。AnaglyphPassNode 在这个通用立体渲染框架之上,只负责两件事:

  1. 重写 updateStereoCamera():用 frameCorners 代替默认的 StereoCamera.update(),实现离轴投影;
  2. 重写 setup():提供 TSL(Three Shading Language)片段着色器,用一对 3×3 颜色矩阵把左右眼颜色混合为最终像素。

2. 导入方式

AnaglyphPassNode 属于 addon(examples/jsm 下的扩展模块),必须显式导入(参考官方手册 Installation#Addons)。模块导出三样东西:

  • AnaglyphPassNode 类(默认导出);
  • AnaglyphAlgorithm 算法枚举;
  • AnaglyphColorMode 色卡枚举;
  • anaglyphPass TSL 函数(语法糖,等价于 new AnaglyphPassNode(scene, camera))。
import { anaglyphPass, AnaglyphAlgorithm, AnaglyphColorMode } from 'three/addons/tsl/display/AnaglyphPassNode.js';

注意该模块内部从 'three/webgpu''three/tsl' 导入依赖(见 源文件首行),因此它运行在 WebGPU 渲染器(WebGPURenderer)+ RenderPipeline 体系下,而非经典 WebGLRenderer + EffectComposer 体系。

3. 构造函数与属性速查

3.1 构造函数

new AnaglyphPassNode( scene : Scene, camera : Camera )
  • scene:要渲染的场景;
  • camera:用于渲染场景的相机(内部会基于它派生左右眼相机与虚拟屏幕)。

3.2 属性一览

属性 类型 默认值 说明
.algorithm string(getter/setter) 'dubois' 当前色差合成算法,取值见第 5 节的 AnaglyphAlgorithm 枚举;setter 变更时自动重建左右颜色矩阵
.colorMode string(getter/setter) 'redCyan' 当前色卡模式,取值见 AnaglyphColorMode 枚举;同样在变更时重建矩阵
.eyeSep number 0.064 瞳距(eye separation),世界单位。典型人类瞳距为 0.064 米(64mm)
.planeDistance number 0.5 观察点到虚拟屏幕平面的距离(零视差面)。该距离处的物体正好落在"屏幕表面";更近的物体出现在屏幕前方(负视差),更远的物体在屏幕后方(正视差)。屏幕尺寸会由相机 FOV 与宽高比在该距离处推导,确保立体视图与相机视场一致
.isAnaglyphPassNode boolean(readonly) true 类型测试标志

构造函数源码 可以确认几个实现细节:

  • _algorithm_colorMode 是私有状态,通过 getter/setter 暴露;setter 中做了"值变化才更新"的短路判断(if ( this._algorithm !== value )),变更时调用 _updateMatrices()
  • 左右眼颜色矩阵各自是一个 TSL uniform( new Matrix3() ) 节点(_colorMatrixLeft / _colorMatrixRight),构造函数末尾立即 _updateMatrices() 初始化为默认的 dubois + redCyan 组合;
  • _updateMatrices() 的查表逻辑为 ANAGLYPH_MATRICES[ algorithm ][ colorMode ],再分别 fromArray 写入左右两个 Matrix3L432-L439)。

4. 立体渲染管线:父类如何工作

理解 AnaglyphPassNode 之前,先看父类 StereoCompositePassNode 的每帧流程,这解释了子类为什么只需提供"相机更新 + 合成着色器"两个钩子。

父类持有:

  • this.stereo = new StereoCamera()——内部立体相机;
  • 两个半精度浮点渲染目标 _renderTargetL / _renderTargetRHalfFloatType,保证 HDR 中间结果),以及对应的 TSL 纹理节点 _mapLeft / _mapRight
  • 一个 QuadMesh,用于把合成结果全屏绘制。

每帧在 updateBefore( frame ) 中执行(L133-L169):

  1. 保存并重置渲染器状态(RendererUtils.resetRendererState),读取当前 pixelRatio
  2. 调用 this.updateStereoCamera( renderer.coordinateSystem )——这正是子类重写离轴投影的切入点
  3. 按渲染器当前尺寸同步两个渲染目标尺寸;
  4. 分别把场景用 stereo.cameraL / stereo.cameraR 渲染进左右两个渲染目标;
  5. 切回 Pass 的输出目标,把 QuadMesh 的材质设为子类的 _material 并渲染——完成"左右图 → 单图"的合成;
  6. 恢复渲染器状态。

dispose() 会释放两个渲染目标与合成材质,Pass 不再使用时应调用。

5. 核心原理一:离轴立体投影(updateStereoCamera)

文档标注 AnaglyphPassNode Overrides StereoCompositePassNode#updateStereoCamera。父类的默认实现只是把 coordinateSystem 同步给双眼后调用 this.stereo.update( this.camera )(即传统 StereoCamera 的等距平行投影);而 AnaglyphPassNode重写版本(L447-L502) 实现了"物理正确"的对屏(to-screen)几何:

updateStereoCamera( coordinateSystem ) {
	const { stereo, camera } = this;

	stereo.cameraL.coordinateSystem = coordinateSystem;
	stereo.cameraR.coordinateSystem = coordinateSystem;

	// 从相机世界矩阵取出局部坐标轴
	camera.matrixWorld.extractBasis( _right, _up, _forward );
	_right.normalize(); _up.normalize(); _forward.normalize();

	// 1) 双眼位置:沿相机右轴各偏移半个瞳距
	const halfSep = this.eyeSep / 2;
	_eyeL.copy( camera.position ).addScaledVector( _right, -halfSep );
	_eyeR.copy( camera.position ).addScaledVector( _right,  halfSep );

	// 2) 屏幕中心:相机前方 planeDistance 处
	_screenCenter.copy( camera.position ).addScaledVector( _forward, -this.planeDistance );

	// 3) 屏幕尺寸:由 FOV 与宽高比在 planeDistance 处推导
	const halfHeight = this.planeDistance * Math.tan( DEG2RAD * camera.fov / 2 );
	const halfWidth  = halfHeight * camera.aspect;

	// 4) 由中心点 ± 半宽/半高得到三个角点(右下、左下、左上)
	// ...
	// 5) 每个眼睛相机:near/far 继承主相机,然后用 frameCorners 精确瞄准屏幕角点
	frameCorners( stereo.cameraL, _screenBottomLeft, _screenBottomRight, _screenTopLeft, true );
	stereo.cameraL.matrixWorld.compose( ... );
	stereo.cameraL.matrixWorldInverse.copy( stereo.cameraL.matrixWorld ).invert();
	// cameraR 同理
}

关键步骤拆解:

  • 眼位:双眼沿相机世界系右轴对称分布,间距即 eyeSep,与真实人眼几何一致;
  • 虚拟屏幕:中心在相机正前方 planeDistance 处;半高 = planeDistance · tan(fov/2),半宽 = 半高 × aspect。这一步保证了立体视图覆盖范围与原相机视场完全一致,也解释了文档中"screen dimensions are derived from the camera's FOV and aspect ratio at this distance"的说法;
  • frameCorners:把每个眼睛相机的投影矩阵和朝向直接"对准"屏幕的三个角点。由于眼睛并不在屏幕中心正后方,左右眼看到的是旋转且投影中心偏移的画面——这正是"off-axis"的含义,也是它优于传统平行投影(会导致梯形失真)的地方。

frameCorners() 实现 的核心是:由屏幕角点构造屏幕的右轴 _vr、上轴 _vu、法线 _vn,计算眼点到屏幕各边缘的投影距离 l / r / b / t,然后:

  1. 用四元数把相机的 +Y 轴对齐 _vu、+Z 轴对齐 _vn,使焦平面与屏幕共面;
  2. 直接手写非对称(off-axis)透视投影矩阵,其水平/垂直中心由 (r+l)/(r-l)(t+b)/(t-b) 项表达投影中心的偏移;
  3. estimateViewFrustum === true(本 Pass 传入的就是 true),会对 camera.fov 写一个保守估计值,防止视锥剔除(frustum culling)把超出原始视场裁剪盒的离轴画面裁掉。

另外注意 frameCorners 文档注释明确提醒:它直接覆写投影矩阵,忽略 fov/aspect 等标准参数,调用后不要调用 updateProjectionMatrix()——AnaglyphPassNode 中确实没有调用,这与实现是吻合的。

零视差的直观含义:位于 planeDistance 处的物体,在左右眼画面中投影到同一屏幕点,戴上红青眼镜后恰好"贴"在屏幕表面;planeDistance 因此就是立体效果中的"屏幕深度"旋钮。

6. 核心原理二:7 算法 × 3 色卡的颜色矩阵体系

合成本质是一次逐像素的颜色加权混合。setup() 中生成的 TSL 片段(L510-L533):

const colorL = this._mapLeft.sample( uvNode );
const colorR = this._mapRight.sample( uvNode );

const color = clamp(
	this._colorMatrixLeft.mul( colorL.rgb )
		.add( this._colorMatrixRight.mul( colorR.rgb ) )
);
return vec4( color.rgb, max( colorL.a, colorR.a ) );

output = clamp( M_left × L.rgb + M_right × R.rgb ),透明度取左右眼中较大者。M_leftM_right 两个 3×3 矩阵完全由 (algorithm, colorMode) 决定,全部集中在源文件的 ANAGLYPH_MATRICES 常量表中(L114-L270)。

6.1 枚举取值

// 算法(.algorithm 的合法取值)
const AnaglyphAlgorithm = {
	TRUE: 'true', GREY: 'grey', COLOUR: 'colour', HALF_COLOUR: 'halfColour',
	DUBOIS: 'dubois', OPTIMISED: 'optimised', COMPROMISE: 'compromise'
};

// 色卡(.colorMode 的合法取值)
const AnaglyphColorMode = {
	RED_CYAN: 'redCyan', MAGENTA_CYAN: 'magentaCyan', MAGENTA_GREEN: 'magentaGreen'
};

6.2 各算法的设计取向

矩阵表注释标明了每个算法对应的眼镜通道模型("Paper: Left=..., Right=...")与取舍:

算法 左眼通道 右眼通道 特点
TRUE 纯 R 亮度(Lum)进青通道 经典"真色差":左眼红色、右眼青灰色;右眼无色,立体感清晰但色彩损失明显
GREY 亮度进 R 亮度进 G/B 纯灰度基,无色彩、重影(ghosting)最小
COLOUR 纯 R 纯 G、B 全彩色,但视网膜竞争(retinal rivalry)最严重,易疲劳
HALF_COLOUR 亮度进 R 全彩 G、B 折中:牺牲左眼色彩换取右眼全彩
DUBOIS 最小二乘拟合矩阵(含负系数) 同左 针对特定红青眼镜镜片透射率的最优化矩阵,默认算法;源文件注释注明其矩阵源自 Dubois 的经典研究资料
OPTIMISED 0.7G+0.3B 进 R 全彩 G、B 改进色彩、降低视网膜竞争
COMPROMISE 0.439R+0.447G+0.148B 进 R 加权 RGB 进 G/B 源自 Ahtik 的 Compromise Anaglyph 研究(论文矩阵 [8]),色彩与立体效果的最佳平衡之一

亮度系数采用 ITU-R BT.601 标准:LUMINANCE = { R: 0.299, G: 0.587, B: 0.114 }

6.3 三种色卡的含义

  • redCyan(红/青):最通用的红青眼镜,左红右青;
  • magentaCyan(洋红/青):矩阵中额外引入约 50% 的 B 通道共享(如 b: [ 0, 0, 0.5 ] 或半亮度项 [ 0.15, 0.29, 0.06 ]),减轻纯红青的眼镜色差;
  • magentaGreen(洋红/绿):左眼保留部分 B、右眼只走 G 通道,适配洋红/绿眼镜。

矩阵的存储值得注意:createMatrixPair() 把"输出通道 → 输入通道贡献"的行主序规格转换成 Matrix3.fromArray 要求的列主序 9 元数组(L60-L94),其中 LUM 简写直接展开为 [0.299, 0.587, 0.114]。因此你不需要自己计算矩阵——只要设置 .algorithm.colorMode,setter 会自动查表更新两个 uniform 矩阵,无需手动标记更新。

7. 完整用法(官方示例对照)

官方示例 examples/webgpu_display_stereo.html 同时演示了 stereoPassanaglyphPassparallaxBarrierPass 三种立体效果的切换,其中 Anaglyph 相关部分可直接提炼为如下可运行模板:

import * as THREE from 'three/webgpu';
import { anaglyphPass, AnaglyphAlgorithm, AnaglyphColorMode } from 'three/addons/tsl/display/AnaglyphPassNode.js';

const camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 );
camera.position.z = 3;

const scene = new THREE.Scene();
scene.add( /* ... 你的网格 ... */ );

const renderer = new THREE.WebGPURenderer();
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

const renderPipeline = new THREE.RenderPipeline( renderer );

// 创建 Anaglyph Pass
const anaglyph = anaglyphPass( scene, camera );

// 关键参数
anaglyph.eyeSep = 0.064;          // 瞳距(世界单位),典型人眼 0.064
anaglyph.planeDistance = 3;        // 零视差平面距离:放在你希望"贴屏幕"的深度处
anaglyph.algorithm  = AnaglyphAlgorithm.DUBOIS;      // 默认即为 'dubois'
anaglyph.colorMode  = AnaglyphColorMode.RED_CYAN;     // 默认即为 'redCyan'

renderPipeline.outputNode = anaglyph;   // 把整个管线的输出交给该 Pass

renderer.setAnimationLoop( () => renderPipeline.render() );

window.addEventListener( 'resize', () => {
	camera.aspect = window.innerWidth / window.innerHeight;
	camera.updateProjectionMatrix();
	renderer.setSize( window.innerWidth, window.innerHeight );
} );

官方示例中可交互的参数范围也值得参考(GUI 定义见 示例源码 L149-L173):

  • eyeSep:滑杆范围 0.001 ~ 0.15,步长 0.001
  • planeDistance:滑杆范围 0.5 ~ 10,步长 0.1
  • algorithm / colorMode:下拉框枚举上表全部 7 种算法与 3 种色卡。

使用建议:

  1. planeDistance 对齐场景焦点:把零视差平面设在你希望观众"聚焦"的深度(例如场景中心物体所在距离)。示例中相机在 z=3、物体散布在 ±5 的范围内,故取 3
  2. eyeSep 与场景尺度匹配:默认值 0.064 假设世界单位是米;若你的场景以"米"以外的单位建模,需相应换算,否则立体效果会过强或过弱。
  3. 切换效果时:直接改 renderPipeline.outputNode 并置 renderPipeline.needsUpdate = true 即可(见示例 update() 函数)。
  4. 注意运行环境:该模块依赖 three/webgputhree/tsl 构建入口,需使用 WebGPURenderer + RenderPipeline;在经典 WebGL 路径下对应的老实现是 examples/jsm/effects/AnaglyphEffect.js(基于 EffectComposer),二者同名枚举思路相似,但本 Pass 版本额外提供了离轴投影与更多算法矩阵。

8. 相关文档与源码索引

内容 路径
本 API 文档 docs/pages/AnaglyphPassNode.html.md
核心实现 examples/jsm/tsl/display/AnaglyphPassNode.js
父类(立体复合 Pass 框架) examples/jsm/tsl/display/StereoCompositePassNode.js
离轴投影工具函数 examples/jsm/utils/CameraUtils.js
可交互官方示例 examples/webgpu_display_stereo.html
WebGL 版对应实现 examples/jsm/effects/AnaglyphEffect.js
TSL 总览文档 docs/pages/TSL.html.md

小结AnaglyphPassNode 的价值在于两点工程化封装——用 frameCorners() 把"瞳距 + 零视差平面"直接翻译成离轴投影矩阵,让立体几何在任意相机姿态下都保持正确;用查表式的 algorithm × colorMode 矩阵对,把学术界的多种色差合成策略变成两个属性赋值即可完成切换的操作。理解其父类 StereoCompositePassNode 的双渲染目标 + 全屏合成的管线骨架后,你也能以同样模式扩展出其他左右图合并类效果。

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