首页
/ three.js TSL 后处理中的 RenderOutputNode:在任意位置手动执行色调映射与色彩空间转换

three.js TSL 后处理中的 RenderOutputNode:在任意位置手动执行色调映射与色彩空间转换

2026-09-07 10:49:50作者:翟萌耘Ralph

在 three.js 的 WebGPU 渲染管线中,色调映射(tone mapping)与色彩空间转换默认发生在像素写入屏幕帧缓冲的最后一刻。但对于 FXAA 这类需要 sRGB 输入的后期特效,这种"默认时机"往往太迟。RenderOutputNode(及其 TSL 工厂函数 renderOutput())正是为了解决这一矛盾而生:它允许开发者在后期处理链的任意位置手动插入"输出色彩变换",从而精确控制特效处理与色彩输出的先后顺序。本文以 RenderOutputNode.html.md 为骨架,结合 RenderOutputNode.js 源码与仓库内真实示例,讲解该节点的原理、用法与底层实现。读完你将掌握:如何通过 renderOutput() 手动接管色调映射与色彩空间转换,以及它与 RenderPipeline.outputColorTransform 的协作机制。

一、为什么需要"提前"做输出色彩变换

在默认情况下,three.js 的渲染器在像素最终输出到默认(屏幕)帧缓冲之前,会自动完成色调映射与色彩空间转换。对于多数场景,这种"最后一步再变换"是正确且高效的。

但问题出在后期处理链的中间环节

  • 场景渲染出的中间结果处于工作色彩空间(working color space,通常是线性 sRGB)之下;
  • 某些特效需要在该空间之外工作,例如 FXAA 抗锯齿算法要求 sRGB 输入才能正确判断边缘;
  • 如果等整条特效链执行完毕才做色彩变换,FXAA 拿到的仍是线性空间的 HDR 颜色,抗锯齿效果就会出错。

RenderOutputNode 允许把"色调映射 + 工作空间到输出色彩空间转换"这个操作提前到链中的任意节点执行,示例代码如下:

const postProcessing = new RenderPipeline( renderer );
postProcessing.outputColorTransform = false;

const scenePass = pass( scene, camera );
const outputPass = renderOutput( scenePass );

postProcessing.outputNode = outputPass;

关键前提:一旦改用该节点手动执行色彩变换,就必须把 RenderPipelineoutputColorTransform 属性置为 false(默认是 true),否则管线末端仍会再次自动执行一次色调映射与色彩空间转换,造成颜色被处理两遍。

二、最小可运行接入示例

结合后处理管线,一个完整的使用流程如下:

import { RenderPipeline } from 'three/webgpu';
import { pass, renderOutput } from 'three/tsl';

const postProcessing = new RenderPipeline( renderer );
postProcessing.outputColorTransform = false;   // 关闭管线末端的自动输出变换

const scenePass = pass( scene, camera );
const outputPass = renderOutput( scenePass );  // 在场景 Pass 之后立刻做输出变换

postProcessing.outputNode = outputPass;

需要说明两点接入细节:

  1. RenderPipeline 接管渲染后,动画循环中应调用 postProcessing.render() 而非 renderer.render()(参见 RenderPipeline.js 中相关注释与 render() 实现);
  2. 运行时若修改了 outputNodeoutputColorTransform,需要把 postProcessing.needsUpdate 置为 true,触发 _updateContext() 重建内部节点图(见源码 L77-L82 的属性说明与 L216-L240 的 _update() 逻辑)。

三、TSL 工厂函数与链式调用

在 TSL(Three Shading Language)中,你通常不会直接 new RenderOutputNode(...),而是使用同名导出函数(源码 RenderOutputNode.js):

export const renderOutput = ( color, toneMapping = null, outputColorSpace = null ) =>
	new RenderOutputNode( nodeObject( color ), toneMapping, outputColorSpace );

addMethodChaining( 'renderOutput', renderOutput );

要点:

  • 三个参数均可省略:color 默认取 null(构造时使用传入的节点),toneMappingoutputColorSpace 默认 null,表示回退到渲染上下文中携带的值(详见下文"参数回退机制");
  • nodeObject( color ) 负责把普通值/数组包装成 Node 对象;
  • 通过 addMethodChaining 注册后,renderOutput 也可以作为链式方法挂在任意节点上使用,例如 someColorNode.renderOutput(),TSL 中所有方法链工具正是由此模式而来(相关导出位于 TSLBase.js)。

四、构造函数与属性详解

new RenderOutputNode( colorNode : Node, toneMapping : number, outputColorSpace : string )

构造一个新输出节点,输出类型为 vec4(构造函数中调用 super( 'vec4' ))。参数含义:

参数 类型 说明
colorNode Node 待处理的颜色节点(场景 Pass 的输出)
toneMapping number 色调映射类型(null 时回退到上下文)
outputColorSpace string 输出色彩空间(null 时回退到上下文)

.colorNode : Node

待处理的颜色节点。在 setup() 中,若该值为空,会退化使用渲染上下文里的 context.color(源码 L110)。

.isRenderOutputNode : boolean(只读)

类型测试标志,恒为 true。TSL 节点系统中各节点以此类标志做运行时类型判断,默认值 true

.outputColorSpace : string

输出色彩空间。注意:toneMapping 在构造函数中被存到了私有成员 _toneMapping,而 outputColorSpace 则作为公开属性直接暴露——因此公开 API 只提供了 setToneMapping()/getToneMapping() 这一对方法,却没有对应的 colorSpace 访问器,这是类设计上值得注意的不对称之处(见源码 L63-L70)。

五、方法说明

.getToneMapping() : number

返回内部 _toneMapping,即当前生效的色调映射类型(源码 L102-L106)。

.setToneMapping( value : number ) : RenderOutputNode

设置色调映射类型并返回 this,便于链式调用(源码 L89-L95)。返回值类型在文档中标注为 ToneMappingNode,但实际返回的是节点自身引用。

可传入的色调映射类型定义在 constants.js

  • NoToneMapping = 0:不进行色调映射;
  • LinearToneMapping = 1
  • ReinhardToneMapping = 2
  • ACESFilmicToneMapping = 4(r173+ 渲染器的默认映射,常用于电影级观感)。

六、底层处理管线:setup() 中到底发生了什么

要正确使用该节点,理解其内部处理顺序至关重要。setup()(源码 RenderOutputNode.js)按以下固定顺序构造输出:

setup( { context } ) {
	// 1) 取输入颜色,缺省回退到 context.color
	let outputNode = this.colorNode || context.color;

	// 2) 将 alpha 钳制到 [0,1]
	outputNode = vec4( outputNode.rgb, outputNode.a.clamp( 0.0, 1.0 ) );

	// 3) 反预乘 alpha(把预乘颜色还原为非预乘形式)
	outputNode = unpremultiplyAlpha( outputNode );

	// 4) 色调映射(节点参数优先,其次取渲染上下文)
	const toneMapping = ( this._toneMapping !== null ? this._toneMapping : context.toneMapping ) || NoToneMapping;
	if ( toneMapping !== NoToneMapping ) {
		outputNode = outputNode.toneMapping( toneMapping );
	}

	// 5) 工作色彩空间 → 输出色彩空间
	const outputColorSpace = ( this.outputColorSpace !== null ? this.outputColorSpace : context.outputColorSpace ) || NoColorSpace;
	if ( outputColorSpace !== NoColorSpace && outputColorSpace !== ColorManagement.workingColorSpace ) {
		outputNode = outputNode.workingToColorSpace( outputColorSpace );
	}

	// 6) 在输出色彩空间中重新预乘 alpha
	outputNode = premultiplyAlpha( outputNode );

	return outputNode;
}

整个链路可拆解为六个步骤,含义如下:

  1. 取色:优先用传入的 colorNode,为空则从渲染上下文取 context.color
  2. Alpha 钳制:保证 alpha 落在合法范围 [0, 1]
  3. 反预乘(unpremultiply):若上游颜色是预乘 alpha(premultiplied alpha)形式,先还原为非预乘形式,避免后续处理在乘过 alpha 的 RGB 上进行。unpremultiplyAlpha 的实现见 PremultiplyAlphaFunctions.js,其用 a.equal(0).select(vec4(0), vec4(rgb.div(a), a)) 处理了 alpha 为 0 时除零的边界情况;
  4. 色调映射:HDR 线性颜色在此被映射到 LDR 范围。toneMapping() 方法与 workingToColorSpace() 同源于 ColorSpaceNode.js 中注册的链式方法;
  5. 色彩空间转换:从工作色彩空间转换到输出色彩空间(通常是 SRGBColorSpace = 'srgb',见 constants.js)。若目标空间等于 ColorManagement.workingColorSpace 或为 NoColorSpace,则跳过本次转换;
  6. 重新预乘:在输出色彩空间中再次预乘 alpha,保证与混合(blending)等下游合成操作兼容。

这种"先反预乘 → 变换 → 再预乘"的顺序保证了色调映射和色彩空间转换始终作用在非预乘的线性颜色上,数学上最为准确,也解释了为什么该节点要内联引入 premultiplyAlpha/unpremultiplyAlpha 两个工具函数。

七、参数回退机制:与 RenderPipeline 的协作

前文提到,renderOutput() 的两个默认参数为 null。它们回退到 context.toneMappingcontext.outputColorSpace,这套机制需要看 RenderPipeline 的实现才能串起来。

RenderPipeline.js_updateContext() 中,当 outputColorTransform === false 时,管线不会在末端包裹 renderOutput(),而是把渲染器的色调映射与色彩空间快照放进 contextData 供下游读取:

} else {
	contextData.toneMapping = toneMapping;        // 渲染器当时的色调映射
	contextData.outputColorSpace = outputColorSpace; // 渲染器当时的输出色彩空间
}

因此,当你写 renderOutput( scenePass ) 而不显式传第二、第三参数时,节点会自动读取渲染上下文里"当前渲染器状态"对应的色调映射类型与输出色彩空间——这正是上一条中 context.toneMapping / context.outputColorSpace 的来源。

RenderPipeline 构造时还会把 renderer.toneMappingrenderer.outputColorSpace 的快照存为内部状态(源码 L112-L120),并在 render() 期间临时将渲染器切到 NoToneMapping + 工作色彩空间,渲染完 quad 后再恢复(源码 L138-L156),从而保证"默认自动变换"不会和节点的手动变换叠加。而对直接的 WebGPU 渲染(DirectRenderPipeline.js),其 getOutput() 回调同样遵守:outputColorTransform === true 时才调用 renderOutput( ... ) 包裹最终输出,否则直接透传管线输出节点。

一言以蔽之:outputColorTransform = false 是"你手动接管输出变换"的开关,而 RenderOutputNode 就是你接管变换的手。

八、实战参考:仓库中的真实用法

仓库中最典型的用法出现在 FXAA 示例 examples/webgpu_postprocessing_fxaa.html 中:

renderPipeline.outputColorTransform = false;         // 关键:关闭自动变换

const scenePass = pass( scene, camera );
const outputPass = renderOutput( scenePass );        // 先做色调映射 + sRGB 转换

// 场景 Pass 之后紧跟 FXAA,让 FXAA 吃到 sRGB 输入
const fxaaPass = fxaa( outputPass );
renderPipeline.outputNode = fxaaPass;

该示例还演示了运行时用 GUI 切换 renderPipeline.outputNode(在 fxaaPassoutputPass 之间切换对比)的交互式验证方式——这正是"在任意位置应用输出变换"的直观演示。

除 FXAA 外,仓库内下列示例也使用了 renderOutput()outputColorTransform = false,可作为扩展阅读:

九、小结与使用清单

RenderOutputNode 的价值在于把 three.js 渲染末端"自动化"的色彩变换改造成"可插拔、可定位"的显式节点。实际使用时请对照以下清单排查:

  1. new RenderPipeline( renderer ) 创建管线,并把 outputColorTransform 设为 false
  2. const outputPass = renderOutput( scenePass )(或 scenePass.renderOutput())在需要的位置生成输出节点;
  3. 将该节点(或在其后继续叠加的特效节点)赋给 postProcessing.outputNode
  4. 若运行时动态替换 outputNode,记得将 postProcessing.needsUpdatetrue 强制重建;
  5. 需要覆盖渲染器默认设置时,显式传入 renderOutput( scenePass, ACESFilmicToneMapping, SRGBColorSpace ),否则保持 null 走上下文回退即可。

掌握该节点的内部顺序(钳制 alpha → 反预乘 → 色调映射 → 色彩空间转换 → 预乘)与回退机制,你就能在 TSL 后期特效链中精准控制每一处颜色处理的发生位置,避免"色彩被处理两遍"或"FXAA 拿到线性空间输入"这两类最常见的管线问题。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389