three.js WebGPU 色彩分级:Lut3DNode 三维查找表(3D LUT)后处理节点全解析
2026/9/8 23:21:13 网站建设 项目流程

three.js WebGPU 色彩分级:Lut3DNode 三维查找表(3D LUT)后处理节点全解析

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

本文围绕 three.js 节点材质与后处理体系中的Lut3DNode展开,讲解如何用三维查找表(Look-Up Table,LUT)对 WebGPU 渲染管线输出做专业级色彩分级(color grading)。你将学会它的导入方式、构造函数与 TSL 工厂函数lut3D()、节点属性、setup()背后的采样着色算法,以及如何把它接入RenderPipeline后处理链并实时切换 LUT,最终把商业 LUT 文件(.CUBE/.3dl/ PNG)变成可复用的电影感滤镜。

一、Lut3DNode 是什么

Lut3DNode是一个用于通过查找表进行色彩分级的后处理节点(post processing node for color grading via lookup tables)。它的类继承链为EventDispatcher → Node → TempNode → Lut3DNode,本质是一个从 TempNode 派生的临时输出节点,构造时声明输出类型为'vec4',即它返回 RGBA 颜色。

  • 源码位置:examples/jsm/tsl/display/Lut3DNode.js
  • 静态类型标识:Lut3DNode(由static get type()返回)
  • 所属目录:examples/jsm/tsl/display/,与FXAANodeBloomNodeSSAONodeSMAANode等 TSL 显示后处理节点并列

与传统的"把 LUT 硬编码进 ShaderMaterial"方案不同,Lut3DNode 被设计为 TSL(Three Shading Language)节点图的一部分:你可以把它当作一条可编程后处理链中的一环,输入任意颜色节点、输出一个vec4结果节点,最终赋给RenderPipelineoutputNode

二、导入方式与使用前提

Lut3DNode 属于插件模块(addon),必须显式导入:

import { lut3D } from 'three/addons/tsl/display/Lut3DNode.js';

使用时需要引入对应的 WebGPU 构建与 TSL 工具函数。参考官方示例 examples/webgpu_postprocessing_3dlut.html 的 import map 配置:

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

在模块脚本中同时引入节点类、TSL 辅助函数与 LUT 加载器:

import * as THREE from 'three/webgpu'; import { pass, renderOutput, texture3D, uniform } from 'three/tsl'; import { lut3D } from 'three/addons/tsl/display/Lut3DNode.js'; import { LUTCubeLoader } from 'three/addons/loaders/LUTCubeLoader.js'; import { LUT3dlLoader } from 'three/addons/loaders/LUT3dlLoader.js';

从源码结构看,该节点被设计为配合 WebGPU 渲染器(WebGPURenderer)与RenderPipeline的"新后处理栈"使用(manual/pages/webgpu-postprocessing.html),其着色逻辑依赖three/tsl导出的nodeObjectFnfloatuniformvec3vec4mix等原语。

三、构造函数与 TSL 工厂函数

3.1 new Lut3DNode( inputNode, lutNode, size, intensityNode )

new Lut3DNode( inputNode, lutNode, size, intensityNode )
参数类型说明
inputNodeNode表示效果输入(通常是某一条 pass 的颜色输出节点)
lutNodeTextureNode表示查找表的纹理节点(一般由texture3D()包装的 3D 纹理)
sizenumber查找表的尺寸(即 N×N×N 立方体每边像素数)
intensityNodeNode.<float>控制效果强度(0–1,参与与原始颜色的mix

对应源码构造过程(Lut3DNode.js):

constructor( inputNode, lutNode, size, intensityNode ) { super( 'vec4' ); this.inputNode = inputNode; this.lutNode = lutNode; this.size = uniform( size ); // number 被封装为 UniformNode<float> this.intensityNode = intensityNode; }

值得注意的细节:size传入后立刻被uniform( size )封装为UniformNode<float>。这意味着size 可以像 uniform 一样在运行时被改写(见下文"运行时更新"),而无需重建节点——这正是示例中切换不同尺寸 LUT 的实现基础。

3.2 TSL 工厂函数 lut3D()

除了new构造,源码文件底部还导出了一个同名便捷函数(Lut3DNode.js):

export const lut3D = ( node, lut, size, intensity ) => new Lut3DNode( nodeObject( node ), nodeObject( lut ), size, nodeObject( intensity ) );

它等价于"自动把普通值包装成节点对象再构造",其中intensity既可以是Node<float>也可以是普通number(内部经nodeObject归一)。它在 docs/TSL.md 的 TSL 函数表中被记录为lut3D( node, lut, size, intensity ) → 创建 LUT 色彩分级效果,在 TSL.html.md 中有完整 API 条目:

.lut3D( node : Node, lut : TextureNode, size : number, intensity : Node.<float> | number ) : Lut3DNode—— TSL function for creating a LUT node for color grading via post processing.

推荐日常开发使用lut3D(),写法更接近 TSL 惯用风格:

const lutPass = lut3D( outputPass, texture3D( lut.texture3D ), lut.texture3D.image.width, uniform( 1 ) );

四、属性详解

属性类型含义
.inputNodeNode效果输入节点
.lutNodeTextureNode查找表纹理节点
.sizeUniformNode.<float>查找表尺寸(注意它是 uniform,可运行时更新)
.intensityNodeNode.<float>效果强度控制

对应源码中的@type注释(Lut3DNode.js)即可确认。由于这些属性都被直接保留为公共字段而非 getter/setter 封装,运行时可以直接对.value赋值来实时调节,例如:

lutPass.intensityNode.value = 0.6; // 降低分级强度 lutPass.lutNode.value = newLutTexture; // 换一张 LUT lutPass.size.value = newSize; // 同步 LUT 尺寸

五、setup() 方法与内部着色算法

setup( builder )方法用于组装该效果节点的 TSL 代码,覆盖自 TempNode#setup:

.setup( builder : NodeBuilder ) : ShaderCallNodeInternal—— builder 为当前 NodeBuilder;Overrides:TempNode#setup

从实现看(Lut3DNode.js),它并不接收 builder 参数,而是直接返回一个由Fn()函数体调用的ShaderCallNode

setup() { const { inputNode, lutNode } = this; const sampleLut = ( uv ) => lutNode.sample( uv ); const lut3D = Fn( () => { const base = inputNode; // pull the sample in by half a pixel so the sample begins at // the center of the edge pixels. const pixelWidth = float( 1.0 ).div( this.size ); const halfPixelWidth = float( 0.5 ).div( this.size ); const uvw = vec3( halfPixelWidth ).add( base.rgb.mul( float( 1.0 ).sub( pixelWidth ) ) ); const lutValue = vec4( sampleLut( uvw ).rgb, base.a ); return vec4( mix( base, lutValue, this.intensityNode ) ); } ); const outputNode = lut3D(); return outputNode; }

这个函数体揭示了 3D LUT 色彩分级的核心采样算法,共四步:

  1. 像素宽度归一化pixelWidth = 1.0 / sizehalfPixelWidth = 0.5 / sizesize即 LUT 立方体边长,例如 33、64。这里计算的是每个 LUT "格子"在归一化 UV 空间中的宽度。
  2. 坐标收缩与半像素内缩uvw = halfPixelWidth + base.rgb * (1.0 - pixelWidth)。注释明确说明这是为了"把采样点向中心拉进半个像素,使采样从边缘像素的中心开始",避免因线性过滤而在 LUT 立方体的边界处采到越界/混合错误的颜色。从向量运算可以看出:输入的 RGB 被当作三维采样坐标,R→XG→YB→Z分别映射到 3D 纹理的三个采样轴。
  3. 采样与通道保留lutValue = vec4( sampleLut( uvw ).rgb, base.a )。只取 LUT 的 RGB 作为分级后颜色,Alpha 通道沿用输入base.a——即该节点只做颜色映射,不影响透明。
  4. 按强度混合mix( base, lutValue, intensityNode )。当强度为 0 时完全保留原色,为 1 时完全使用 LUT 映射结果,中间值获得渐变的"滤镜浓度",因此可以实现强度为 0–1 的平滑淡入淡出。

六、端到端接入:把 LUT 挂到 RenderPipeline 后处理链

Lut3DNode 的典型使用场景是"3D LUT 色彩分级",官方为此提供了可运行示例 examples/webgpu_postprocessing_3dlut.html(含咖啡杯烘焙场景 + 烟雾着色器,并内置多组商业 LUT 供切换)。

6.1 关闭默认颜色变换,用 renderOutput() 控制顺序

manual/pages/webgpu-postprocessing.html 明确指出:使用后处理时,tone mapping 与色彩空间转换默认会在效果链末端自动应用;如果要对 FXAA、Lut3DNode 做色彩分级,应先关闭自动 tone mapping 与色彩空间转换,再自行用renderOutput()编排顺序。示例中的关键代码:

const renderPipeline = new THREE.RenderPipeline( renderer ); // ignore default output color transform ( toneMapping and outputColorSpace ) renderPipeline.outputColorTransform = false; const scenePass = pass( scene, camera ); const outputPass = renderOutput( scenePass ); // 在这里先完成 tone mapping + 色彩空间转换

由于色彩分级通常作用于色调映射后的 sRGB 画面,示例刻意让 LUT 节点作用在renderOutput()的结果之上。

6.2 构建 LUT 后处理节点并挂到管线

const lut = lutMap[ params.lut ]; // 加载好的 LUT(含 texture3D 与尺寸) lutPass = lut3D( outputPass, // inputNode:renderOutput 的结果 texture3D( lut.texture3D ), // lutNode:3D 纹理经 texture3D() 包装 lut.texture3D.image.width, // size:取 3D 纹理宽度,如 64 uniform( 1 ) // intensityNode:初始强度 1 ); renderPipeline.outputNode = lutPass; // 赋给管线输出

对应的 TSL 一行式写法是lut3D( outputPass, texture3D( lut.texture3D ), lut.texture3D.image.width, uniform( 1 ) )(示例源码)。size直接取 3D 纹理的image.width,因为加载器保证width = height = depth = size

6.3 动画循环内实时更新

后处理链建好后,可以在每帧或交互时动态改写节点属性(示例源码):

async function animate() { controls.update(); lutPass.intensityNode.value = params.intensity; // GUI 拖动的强度 if ( lutMap[ params.lut ] ) { const lut = lutMap[ params.lut ]; lutPass.lutNode.value = lut.texture3D; // 切换不同 LUT lutPass.size.value = lut.texture3D.image.width;// 尺寸随之更新 } renderPipeline.render(); }

配合调试面板:

const gui = renderer.inspector.createParameters( 'Settings' ); gui.add( params, 'lut', Object.keys( lutMap ) ); gui.add( params, 'intensity', 0, 1 );

即可在运行时从多组 LUT 间实时切换、平滑调节滤镜浓度。

七、LUT 数据从哪来:加载器与 Data3DTexture

Lut3DNode 需要一张真实的三维 LUT 纹理(N×N×N 体素立方体),three.js 提供三种加载器,全部位于 examples/jsm/loaders/:

加载器适用格式说明
LUTCubeLoader.CUBE(Adobe/通用 Cube 格式,文本)解析后返回{ title, size, domainMin, domainMax, texture3D }
LUT3dlLoader.3dl(Autodesk 3D LUT 格式,文本)解析结果结构同上
LUTImageLoader排列成网格的 LUT PNG 图片内部拆分/重建为 3D 纹理

LUTCubeLoader.parse()为例(LUTCubeLoader.js),最终会构造一个Data3DTexture

const texture3D = new Data3DTexture(); texture3D.image.data = data; texture3D.image.width = size; texture3D.image.height = size; texture3D.image.depth = size; texture3D.type = this.type; texture3D.magFilter = LinearFilter; // 三线性插值,保证分级过渡平滑 texture3D.minFilter = LinearFilter; texture3D.wrapS = ClampToEdgeWrapping; texture3D.wrapT = ClampToEdgeWrapping; texture3D.wrapR = ClampToEdgeWrapping; texture3D.generateMipmaps = false; texture3D.needsUpdate = true;

这些纹理设置与 Lut3DNode 的采样算法高度配合:

  • LinearFilter的 min/mag让 GPU 对 LUT 体素做三线性插值,这是色彩分级"连续、平滑"的关键;
  • 三个轴全部ClampToEdgeWrapping配合半像素内缩坐标,杜绝坐标越界;
  • 不生成 mipmap,避免 LOD 导致的颜色串扰。

官方示例中的 LUT 素材存放在 examples/luts/ 目录,例如Bourbon 64.CUBE(64³)、Chemical 168.CUBE(168³)、Clayton 33.CUBE(33³)、Cubicle 99.CUBE(99³)、Remy 24.CUBE(24³)以及.3dl格式的Presetpro-Cinematic.3dl等。可见size 没有固定值,随 LUT 文件内容而定,这也是为什么size需要显式传参、并作为可写 uniform 暴露。

八、项目内的高级使用:Inspector 色彩分级扩展

在仓库的 examples/jsm/inspector/extensions/color-grading/ColorGrading.js 中,可以看到 Lut3DNode 在编辑器工具内的另一类用法——把整个 Inspector 面板的调色流程(白平衡、曝光、色轮、曲线、饱和度、对比度等)最终落地为一张 LUT 并交给lut3D()处理:

import { lut3D } from 'three/addons/tsl/display/Lut3DNode.js'; // ... this.lutSize = 32; // 默认生成 32³ 的 LUT this.lutPassNode = null;

并通过对RenderPipeline.prototype.render的 Hook 记录当前 pipeline 后把lut3D节点写入管线输出。这印证了 Lut3DNode 的两个设计取向:

  1. 适合作为整条调色链的"最后一公里"——各种颜色运算先烘焙进一张 LUT 纹理,再由 GPU 用一次三线性采样完成分级,性能极高;
  2. size 语义统一——无论是32³的运行时生成 LUT,还是64³的外部.CUBE文件,都通过同一个sizeuniform 驱动。

九、小结与排错提示

检查项建议值/做法
导入路径three/addons/tsl/display/Lut3DNode.js,仅导出lut3D工厂函数
必须给lutNode3D 纹理texture3D( lut.texture3D )包装Data3DTexture,二维 LUT 贴图不适用
size必须与 LUT 一致直接取lut.texture3D.image.width,切换 LUT 时同步更新size.value
强度调节修改intensityNode.value(0–1),内部以mix()实现
渲染顺序建议outputColorTransform = false+renderOutput(),让 LUT 作用在色调映射后的颜色上
兼容性前提面向 WebGPU 渲染器与 TSL/RenderPipeline 后处理体系,不支持传统 WebGLRenderer 的EffectComposer

进一步参考:

  • 节点源码:examples/jsm/tsl/display/Lut3DNode.js
  • 官方示例:examples/webgpu_postprocessing_3dlut.html
  • 后处理手册:manual/pages/webgpu-postprocessing.html
  • TSL API 索引:docs/pages/TSL.html.md,函数表见 docs/TSL.md
  • LUT 加载器:LUTCubeLoader、LUT3dlLoader、LUTImageLoader
  • 调试扩展实现:ColorGrading.js

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询