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/,与FXAANode、BloomNode、SSAONode、SMAANode等 TSL 显示后处理节点并列
与传统的"把 LUT 硬编码进 ShaderMaterial"方案不同,Lut3DNode 被设计为 TSL(Three Shading Language)节点图的一部分:你可以把它当作一条可编程后处理链中的一环,输入任意颜色节点、输出一个vec4结果节点,最终赋给RenderPipeline的outputNode。
二、导入方式与使用前提
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导出的nodeObject、Fn、float、uniform、vec3、vec4、mix等原语。
三、构造函数与 TSL 工厂函数
3.1 new Lut3DNode( inputNode, lutNode, size, intensityNode )
new Lut3DNode( inputNode, lutNode, size, intensityNode )| 参数 | 类型 | 说明 |
|---|---|---|
inputNode | Node | 表示效果输入(通常是某一条 pass 的颜色输出节点) |
lutNode | TextureNode | 表示查找表的纹理节点(一般由texture3D()包装的 3D 纹理) |
size | number | 查找表的尺寸(即 N×N×N 立方体每边像素数) |
intensityNode | Node.<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 ) );四、属性详解
| 属性 | 类型 | 含义 |
|---|---|---|
.inputNode | Node | 效果输入节点 |
.lutNode | TextureNode | 查找表纹理节点 |
.size | UniformNode.<float> | 查找表尺寸(注意它是 uniform,可运行时更新) |
.intensityNode | Node.<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 色彩分级的核心采样算法,共四步:
- 像素宽度归一化:
pixelWidth = 1.0 / size,halfPixelWidth = 0.5 / size。size即 LUT 立方体边长,例如 33、64。这里计算的是每个 LUT "格子"在归一化 UV 空间中的宽度。 - 坐标收缩与半像素内缩:
uvw = halfPixelWidth + base.rgb * (1.0 - pixelWidth)。注释明确说明这是为了"把采样点向中心拉进半个像素,使采样从边缘像素的中心开始",避免因线性过滤而在 LUT 立方体的边界处采到越界/混合错误的颜色。从向量运算可以看出:输入的 RGB 被当作三维采样坐标,R→X、G→Y、B→Z分别映射到 3D 纹理的三个采样轴。 - 采样与通道保留:
lutValue = vec4( sampleLut( uvw ).rgb, base.a )。只取 LUT 的 RGB 作为分级后颜色,Alpha 通道沿用输入base.a——即该节点只做颜色映射,不影响透明。 - 按强度混合:
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 的两个设计取向:
- 适合作为整条调色链的"最后一公里"——各种颜色运算先烘焙进一张 LUT 纹理,再由 GPU 用一次三线性采样完成分级,性能极高;
- size 语义统一——无论是
32³的运行时生成 LUT,还是64³的外部.CUBE文件,都通过同一个sizeuniform 驱动。
九、小结与排错提示
| 检查项 | 建议值/做法 |
|---|---|
| 导入路径 | three/addons/tsl/display/Lut3DNode.js,仅导出lut3D工厂函数 |
必须给lutNode传3D 纹理 | 用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),仅供参考