Three.js 颜色管理:别让你的 3D 场景忽明忽暗、颜色发灰
原文出处:Three.js Manual – Color Management
本文基于官方手册「Color Management」章节整理,用通俗方式带你看懂:为什么同样是红色,贴进场景里就变暗了?为什么开了后期处理画面反而发白?
写在前面
如果你写过几个 Three.js 场景,大概率撞见过下面这些诡异现象:
- 用取色器挑了个漂亮的红,贴上去却发暗、发灰;
- 加了
EffectComposer后期处理之后,整体画面突然「过曝」变白; - 法线贴图、粗糙度贴图看着一切正常,但颜色贴图一换就出问题。
这些都不是你眼睛的问题,也不是材质写错了——根子都在颜色管理(Color Management)。这一篇我们就把这件事聊透:颜色空间到底是什么、Three.js 里每一类数据该用什么空间、以及在代码里怎么配置才不踩坑。
一、先搞懂:什么是「颜色空间」
说白了,一个颜色空间就是一组「设计决策」的组合,目的是在有限精度下表示尽量多的颜色,同时适配显示设备的物理限制。它通常由三块拼起来:
- 原色(Color primaries):红绿蓝并不是绝对的,而是从可见光谱里挑出来的。颜色就是这三个原色的比例。
- 白点(White point):
R=G=B时人眼应该看到「没有颜色」的白或灰。sRGB 的白点定在D65(标准日光)。 - 传递函数(Transfer functions):数值怎么映射到实际亮度?
r=0.5是物理亮度少一半,还是人眼觉得「暗一半」?这两者不一样,需要用数学函数描述。sRGB 用的是非线性传递函数(常被近似成 gamma,但官方建议别用「gamma」这个词,容易歧义)。
加上两个常用术语:
- 颜色模型(Color model):怎么用数字表示颜色,比如 RGB 模型用
r,g,b ∈ [0,1](闭区间)或[0,∞](开区间,能表示超亮的高动态范围)。 - 色域(Color gamut):选定原色和白点后,能表示的颜色范围就是一个「体积」。超出这个体积的叫「超出色域」,闭区间 RGB 表示不了。
重点来了:Three.js 里最常打交道的SRGBColorSpace(sRGB)和LinearSRGBColorSpace(Linear-sRGB),它们的原色和白点完全一样,色域也相同,唯一的区别就在传递函数:
LinearSRGBColorSpace:相对物理光强是线性的;SRGBColorSpace:非线性,更接近人眼感知和常见显示器的表现。
为什么要区分?因为光照计算等渲染操作必须在线性空间里做。但线性颜色存进图片/帧缓冲很浪费精度,人眼直接看也不对劲。所以结论很清晰:输入贴图和最终渲染图一般用非线性 sRGB,而渲染计算过程用线性 Linear-sRGB。
ℹ️ 提示:虽然现在有 Display-P3 等更广色域的屏幕,但 Web 图形 API 基本还是基于 sRGB。用 Three.js 一般只需关心 sRGB 和 Linear-sRGB 这两个。
二、颜色空间的三种「角色」
线性工作流(现代渲染必备)通常会涉及不止一个颜色空间,各自有固定职责。
1. 输入颜色空间(Input)
给 Three.js 的颜色——取色器、贴图、3D 模型——都有自己的颜色空间。不在 Linear-sRGB 工作空间的,都得先转换,贴图还要标上正确的texture.colorSpace。开启下面这行后,十六进制和 CSS 颜色会自动转:
THREE.ColorManagement.enabled=true;注意:这个开关默认就是
true,但显式写出来是个好习惯。
不同数据的归属:
| 数据类型 | 所在颜色空间 | 怎么写 |
|---|---|---|
| 材质/灯光/着色器里的颜色 | Linear-sRGB | 自动(工作空间) |
顶点颜色BufferAttribute | Linear-sRGB | 自动 |
颜色贴图(PNG/JPEG 的.map、.emissiveMap) | sRGB(闭区间) | texture.colorSpace = SRGBColorSpace |
浮点数据贴图(OpenEXR 的.envMap、.lightMap) | Linear-sRGB(开区间,可超 1) | texture.colorSpace = LinearSRGBColorSpace |
非颜色贴图(.normalMap、.roughnessMap) | 无颜色空间 | texture.colorSpace = NoColorSpace |
⚠️ 警告:很多 3D 模型格式没有正确、一致地标注颜色空间,老格式尤其容易出问题。建议优先用glTF 2.0(
GLTFLoader),并尽早用在线查看器验证资源本身是否正确。
2. 工作颜色空间(Working)
渲染、插值等计算,必须在开区间、线性的 Linear-sRGB工作空间里进行——这里的 RGB 分量正比于物理光照。Three.js 的工作空间固定是LinearSRGBColorSpace。
3. 输出颜色空间(Output)
最终输出到屏幕、图片或视频时,要从 Linear-sRGB 工作空间转换出去,由WebGLRenderer.outputColorSpace决定:
- 显示:写到 WebGL canvas 的颜色应该是 sRGB;
- 图片:PNG/JPEG 成品一般用 sRGB;含发光、光照图等超出
[0,1]的数据则用开区间 Linear-sRGB(如 OpenEXR)。
renderer.outputColorSpace=THREE.SRGBColorSpace;// 默认值,通常不用改用了后期处理(post-processing)就必须加
OutputPass,否则输出转换不会自动发生。另外,渲染到 RenderTarget 时,sRGB 更省精度(8 位常够用),Linear-sRGB 往往需要 ≥12 位(half float)。
如果你自己写ShaderMaterial/RawShaderMaterial,输出颜色空间的转换得自己实现。对ShaderMaterial,在片元着色器main()里加上colorspace_fragment着色器块通常就够了。
三、操作 THREE.Color 实例
Color实例的方法默认假设数据已经在 Three.js 的工作空间(Linear-sRGB)里,读写都不会隐式转换。需要手动转就用:
// RGB 分量直接读写,不转换color.r=color.g=color.b=0.5;console.log(color.r);// → 0.5// 手动转换color.r=0.5;color.convertSRGBToLinear();console.log(color.r);// → 0.214041140开启ColorManagement(推荐)后,因为十六进制和 CSS 颜色本质是 sRGB,setter 会自动把输入从 sRGB 转成 Linear-sRGB,getter 返回十六进制/CSS 时又会转回去:
// 十六进制:内部自动存成线性值color.setHex(0x808080);console.log(color.r);// → 0.214041140console.log(color.getHex());// → 0x808080// CSScolor.setStyle('rgb( 0.5, 0.5, 0.5 )');console.log(color.r);// → 0.214041140// 用 colorSpace 参数覆盖默认转换color.setHex(0x808080,THREE.LinearSRGBColorSpace);console.log(color.r);// → 0.5console.log(color.getHex(THREE.LinearSRGBColorSpace));// → 0x808080console.log(color.getHex(THREE.SRGBColorSpace));// → 0xBCBCBC小坑提醒:上面代码里
getHex(SRGBColorSpace)返回0xBCBCBC而不是0x808080,因为「线性 0.5」的 sRGB 编码值本来就是约 0.737,即0xBC。这正说明内部存的是线性值。
四、最常见的几个坑
颜色配置错了,表现很典型:
- 单个颜色或贴图配错→ 它比预期更暗或更亮(比如颜色贴图忘了设
SRGBColorSpace,就会被当成线性值读进来,整体发暗)。 - 渲染器输出空间配错→ 整个场景出问题:
- 缺了到 sRGB 的转换 →整屏偏暗;
- 后期处理里又多转了一次 sRGB(没加
OutputPass却手动转了)→整屏偏亮/过曝。
- 这类问题不均匀,单纯调亮/调暗灯光是解决不了的。
还有一种更隐蔽的:输入空间和输出空间同时配错。这时整体亮度可能「看起来正常」,但颜色会在不同光照下莫名变化,阴影也显得更「硬」、不够柔和。两错并不能抵一对——工作空间必须线性(scene-referred),输出空间必须非线性(display-referred),这是铁律。
总结
一句话记住颜色管理的核心分工:
| 角色 | 颜色空间 | 关键点 |
|---|---|---|
| 输入(颜色贴图) | sRGB | texture.colorSpace = SRGBColorSpace |
| 输入(法线/粗糙度等) | 无 | texture.colorSpace = NoColorSpace |
| 工作(计算) | Linear-sRGB | Three.js 固定,别手动改 |
| 输出(屏幕) | sRGB | renderer.outputColorSpace,后期处理要OutputPass |
| 颜色实例 | Linear-sRGB | 开启ColorManagement后 hex/CSS 自动转 |
实用 checklist:颜色发暗→检查颜色贴图的colorSpace;整屏过曝→检查后期处理有没有OutputPass;模型颜色怪→优先用 glTF 2.0 并早验证。
延伸阅读
- three.js docs – ColorManagement
- three.js docs – WebGLRenderer.outputColorSpace
- three.js docs – Texture.colorSpace
- GPU Gems 3: The Importance of Being Linear
- What every coder should know about gamma
- The Hitchhiker’s Guide to Digital Color