首页
/ three.js 彩色畸变节点 ChromaticAberrationNode 解析:用 TSL 实现镜头色散后处理

three.js 彩色畸变节点 ChromaticAberrationNode 解析:用 TSL 实现镜头色散后处理

2026-09-06 13:43:30作者:明树来

three.js 的 TSL(Three Shading Language)提供了一组显示类后处理节点,其中 ChromaticAberrationNode 用于模拟真实相机镜头产生的"色边"(chromatic aberration)效果:将场景渲染结果的红、绿、蓝三个通道按径向距离分别偏移采样,从而在画面边缘形成彩色的分离光晕。本文基于该节点的 API 文档与仓库源码,完整讲解它的继承体系、构造参数、着色器实现细节,并给出可直接运行的后处理接法。

chromatic aberration 示例运行截图

1. 节点定位与继承关系

ChromaticAberrationNode 是 TSL 显示(display)模块下的一个 addon,继承链为:

EventDispatcher → Node → TempNode → ChromaticAberrationNode

其中 TempNode 是关键的一层:它是一个临时变量类型的节点基类。从源码可以看出,当节点在依赖图中被多处引用时,TempNode#build 会把表达式结果缓存到一个 nodeVar 中,避免重复计算(hasDependencies 通过 usageCount > 1 判断):

// src/nodes/core/TempNode.js
hasDependencies( builder ) {
    return builder.getDataFromNode( this ).usageCount > 1;
}

build( builder, output ) {
    const buildStage = builder.getBuildStage();
    if ( buildStage === 'generate' ) {
        // ... 若存在 propertyName 直接返回缓存变量;
        // 否则若被多处依赖,则把 snippet 写入 lineFlowCode 并缓存
    }
    return super.build( builder, output );
}

对于彩色畸变节点这类内部要对同一张纹理做多次采样的实现,TempNode 的缓存机制保证了它被复用(例如同时接入多个输出分支)时不会重复生成着色器计算。节点构造时调用 super( 'vec4' ),声明其输出类型为 vec4,即标准的 RGBA 颜色。

2. 导入方式

该节点属于 addon,需要显式导入。TSL 函数形式的入口为 chromaticAberration

import { chromaticAberration } from 'three/addons/tsl/display/ChromaticAberrationNode.js';

three npm 包中,该路径对应 examples/jsm/tsl/display/ChromaticAberrationNode.js(仓库内源码文件)。在本地开发时,示例 HTML 通过 importmap 将其映射到 ./jsm/ 目录,例如 examples/webgpu_postprocessing_ca.html

<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>

3. 构造函数与 TSL 函数签名

3.1 构造函数

new ChromaticAberrationNode( textureNode : TextureNode, strengthNode : Node, centerNode : Node, scaleNode : Node )

四个参数均为节点:

参数 类型 说明
textureNode TextureNode 效果的输入,即要施加色散的纹理(通常是上一个后处理 Pass 的输出)
strengthNode Node 效果强度,可传任意标量节点,支持运行时动态修改
centerNode Node 畸变中心点(vec2 UV 坐标),效果从该点向外呈径向发散
scaleNode Node 以中心为基准的分级缩放因子

3.2 TSL 工厂函数

通常不直接 new,而是使用同文件导出的 TSL 函数。其签名与默认值为:

chromaticAberration( node, strength = 1.0, center = null, scale = 1.1 ) : ChromaticAberrationNode
参数 类型 默认值 说明
node Node<vec4> 必填 效果输入节点;可以是 Pass 输出、纹理或其他 TSL 节点
strength Node | number 1.0 强度;越大色彩分离越明显
center Node | Vector2 null(屏幕中心 0.5, 0.5) 畸变中心点
scale Node | number 1.1 分级缩放因子

工厂函数内部会把普通值统一包装为节点对象,这也是 TSL 的典型模式——参数既可以写死数值,也可以换成 uniform 节点实现运行时可调:

// examples/jsm/tsl/display/ChromaticAberrationNode.js
export const chromaticAberration = ( node, strength = 1.0, center = null, scale = 1.1 ) => {

    return nodeObject(
        new ChromaticAberrationNode(
            convertToTexture( node ),
            nodeObject( strength ),
            nodeObject( center ),
            nodeObject( scale )
        )
    );
};

两个关键包装函数值得注意:

  • nodeObject:定义于 src/nodes/tsl/TSLCore.js,把任意值包装成 ShaderNodeObject,使其具备节点的可链式运算能力;
  • convertToTexture:定义于 src/nodes/utils/RTTNode.js,确保输入一定是可采样的纹理节点——若输入已是 SampleNode/TextureNode 则原样返回,若是 PassNode 则取其纹理,否则自动包一层 RTT(Render To Texture):
export const convertToTexture = ( node, ...params ) => {

    if ( node.isSampleNode || node.isTextureNode ) return node;
    if ( node.isPassNode ) return node.getTextureNode();

    return rtt( node, ...params );

};

这意味着你可以把任意 TSL 节点(甚至整条渲染 Pass)直接喂给 chromaticAberration,采样所需的纹理化过程是自动完成的。

4. 实例属性

构造完成后,四个参数以节点属性形式保存在实例上,可在构建着色器前后读取或替换:

属性 类型 说明
.textureNode TextureNode 效果输入纹理节点
.strengthNode Node 强度节点
.centerNode Node 中心点节点
.scaleNode Node 分级缩放节点

由于它们都是节点引用,只要换成 uniform() 创建的 uniform 节点,就能在 CPU 侧随时修改值而无需重新编译管线(见第 6 节示例)。

5. setup 方法与着色器实现原理

5.1 setup 方法

.setup( builder : NodeBuilder ) : ShaderCallNodeInternal

该方法负责生成效果的 TSL 着色器代码,重写了 TempNode#setup(文档中写作 TempNode#setup)。其返回的节点成为 RenderPipeline 输出链中的一次"函数调用"。

5.2 内部算法:三步径向畸变 + 分通道采样

setup 内部定义了一个命名着色器函数 ApplyChromaticAberration,通过 setLayout 声明了类型签名(输入 uv: vec2, strength: float, center: vec2, scale: float,输出 vec4),核心逻辑可以归纳为三层:

// 第一步:计算像素相对中心的径向偏移
const offset = uv.sub( center );
const distance = offset.length();

// 第二步:通道级缩放 —— 红向外扩、绿不动、蓝向内缩
const redScale   = float( 1.0 ).add( scale.mul( 0.02 ).mul( strength ) );
const greenScale = float( 1.0 );
const blueScale  = float( 1.0 ).sub( scale.mul( 0.02 ).mul( strength ) );

// 第三步:附加的色散偏移(正比于 强度 × 距中心距离)
const aberrationStrength = strength.mul( distance );
const rOffset = offset.mul( aberrationStrength ).mul( float( 0.01 ) );
const gOffset = offset.mul( aberrationStrength ).mul( float( 0.0 ) );
const bOffset = offset.mul( aberrationStrength ).mul( float( - 0.01 ) );

随后按三个通道各自的最终 UV 分别采样输入纹理,并把 alpha 通道从原始 UV 处取回:

const finalRedUV   = redUV.add( rOffset );
const finalGreenUV = greenUV.add( gOffset );
const finalBlueUV  = blueUV.add( bOffset );

const r = textureNode.sample( finalRedUV ).r;
const g = textureNode.sample( finalGreenUV ).g;
const b = textureNode.sample( finalBlueUV ).b;
const a = textureNode.sample( uv ).a;

return vec4( r, g, b, a );

从源码结构看,该算法有三个值得注意的设计点:

  1. 色散量随半径增长aberrationStrength = strength * distance 意味着越靠近画面四角,RGB 分离越大,符合真实镜头"边缘色散更明显"的光学特征,中心区域几乎不受影响;
  2. 红蓝对称、绿色为基准:红色通道按 1 + 0.02 * scale * strength 向外放大、蓝色按相反方向缩小,绿色保持原 UV,配合 ±0.01 的径向偏移,形成以绿为基准、红蓝向两侧分流的经典色边;
  3. alpha 保持原始采样:透明度不被畸变,避免边缘出现半透明光晕污染,保证与后续合成 Pass 的兼容性。

uv 的来源也做了兜底处理:优先取 textureNode.uvNode(输入纹理自带的 UV),否则回退到 TSL 内建的 uv() 属性节点(即 src/nodes/accessors/UV.js 中基于 attribute('uv') 的 vec2 节点),因此该节点既可用于全屏后处理,也可用于纹理空间内的局部效果。

6. 实战:在 RenderPipeline 中接入彩色畸变

仓库自带的完整示例是 examples/webgpu_postprocessing_ca.html(在示例索引 examples/files.json 中登记为 webgpu_postprocessing_ca)。其核心接法如下,展示了"场景 Pass → 输出 Pass → 彩色畸变 Pass"的完整链条,以及用 uniform 节点实现运行时调参:

import * as THREE from 'three/webgpu';
import { pass, renderOutput, uniform } from 'three/tsl';
import { chromaticAberration } from 'three/addons/tsl/display/ChromaticAberrationNode.js';

const renderer = new THREE.WebGPURenderer( { antialias: true } );
await renderer.init();

// 后处理管线
const renderPipeline = new THREE.RenderPipeline( renderer );
renderPipeline.outputColorTransform = false;

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

// uniform 节点:CPU 侧可随时修改 .value,无需重编译
const staticStrength = uniform( 1.5 );                    // 示例中强度取 1.5(GUI 范围 0~3)
const staticCenter   = uniform( new THREE.Vector2( 0.5, 0.5 ) );
const staticScale    = uniform( 1.2 );                    // 示例中 scale 取 1.2(GUI 范围 0.5~2)

const caPass = chromaticAberration( outputPass, staticStrength, staticCenter, staticScale );

// 用开关切换是否启用水色散
renderPipeline.outputNode = caPass;

示例中的可调参数范围(由 GUI 配置给出,可作为取值参考):

参数 示例初值 GUI 可调范围
strength 1.5 0 ~ 3
center.x / center.y 0.5 / 0.5 -1 ~ 1
scale 1.2 0.5 ~ 2

示例还演示了两个实用技巧:

  • 动态开关效果renderPipeline.outputNode = enabled ? caPass : outputPass; renderPipeline.needsUpdate = true;,即通过切换输出节点实现 A/B 对比,不需要销毁重建 Pass;
  • 场景内容:示例场景由金属质感的彩色几何体(金属度 0.8、粗糙度 0.2 的多种颜色)加 GridHelper 与粒子组成——高饱和、高对比的物体正好用于观察色边效果,这也是调参时的一个好场景参照。

一个完整的可运行骨架(省略场景搭建):

import * as THREE from 'three/webgpu';
import { pass, renderOutput, uniform } from 'three/tsl';
import { chromaticAberration } from 'three/addons/tsl/display/ChromaticAberrationNode.js';

const renderer = new THREE.WebGPURenderer();
document.body.appendChild( renderer.domElement );
await renderer.init();

const scene  = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 45, innerWidth / innerHeight, 0.1, 200 );
camera.position.set( 0, 15, 40 );

const pipeline   = new THREE.RenderPipeline( renderer );
const outputPass = renderOutput( pass( scene, camera ) );

const strength = uniform( 1.0 );
const scale    = uniform( 1.1 );
pipeline.outputNode = chromaticAberration( outputPass, strength, null, scale );

renderer.setAnimationLoop( () => {
    strength.value = 1.0 + Math.sin( performance.now() * 0.001 ) * 0.5; // 呼吸式色散
    pipeline.render();
};

其中 centernull 时使用默认的屏幕中心(0.5, 0.5)。

7. 注意事项与适用前提

  1. 仅适用于 WebGPU/TSL 体系:该节点基于 three/webgputhree/tsl 的节点构建流程工作,示例均以 WebGPURenderer + RenderPipeline 为宿主;
  2. 输入自动纹理化:依赖 convertToTexture,传入普通节点时可能引入一次 RTT,接在 pass() 之后则直接复用 Pass 纹理,无额外开销;
  3. 强度与比例常量:0.02 / 0.01 等偏移系数是硬编码在着色器函数中的,strengthscale 控制的是其缩放比例;从源码结构看,如需更激进的色散效果,需要调大 strength(示例 GUI 上限为 3);
  4. alpha 不参与畸变:透明度始终来自原始 UV,若你的 Pass 输出依赖 alpha 混合,行为是稳定可预期的;
  5. 复用安全性:继承自 TempNode,同一节点被多处引用时自动走变量缓存路径,不会重复展开采样表达式。

8. 相关资源

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