Three.js 颜色管理:别让你的 3D 场景忽明忽暗、颜色发灰

发布时间:2026/9/3 15:32:57
Three.js 颜色管理:别让你的 3D 场景忽明忽暗、颜色发灰 Three.js 颜色管理别让你的 3D 场景忽明忽暗、颜色发灰原文出处Three.js Manual – Color Management本文基于官方手册「Color Management」章节整理用通俗方式带你看懂为什么同样是红色贴进场景里就变暗了为什么开了后期处理画面反而发白写在前面如果你写过几个 Three.js 场景大概率撞见过下面这些诡异现象用取色器挑了个漂亮的红贴上去却发暗、发灰加了EffectComposer后期处理之后整体画面突然「过曝」变白法线贴图、粗糙度贴图看着一切正常但颜色贴图一换就出问题。这些都不是你眼睛的问题也不是材质写错了——根子都在颜色管理Color Management。这一篇我们就把这件事聊透颜色空间到底是什么、Three.js 里每一类数据该用什么空间、以及在代码里怎么配置才不踩坑。一、先搞懂什么是「颜色空间」说白了一个颜色空间就是一组「设计决策」的组合目的是在有限精度下表示尽量多的颜色同时适配显示设备的物理限制。它通常由三块拼起来原色Color primaries红绿蓝并不是绝对的而是从可见光谱里挑出来的。颜色就是这三个原色的比例。白点White pointRGB时人眼应该看到「没有颜色」的白或灰。sRGB 的白点定在D65标准日光。传递函数Transfer functions数值怎么映射到实际亮度r0.5是物理亮度少一半还是人眼觉得「暗一半」这两者不一样需要用数学函数描述。sRGB 用的是非线性传递函数常被近似成 gamma但官方建议别用「gamma」这个词容易歧义。加上两个常用术语颜色模型Color model怎么用数字表示颜色比如 RGB 模型用r,g,b ∈ [0,1]闭区间或[0,∞]开区间能表示超亮的高动态范围。色域Color gamut选定原色和白点后能表示的颜色范围就是一个「体积」。超出这个体积的叫「超出色域」闭区间 RGB 表示不了。重点来了Three.js 里最常打交道的SRGBColorSpacesRGB和LinearSRGBColorSpaceLinear-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.enabledtrue;注意这个开关默认就是true但显式写出来是个好习惯。不同数据的归属数据类型所在颜色空间怎么写材质/灯光/着色器里的颜色Linear-sRGB自动工作空间顶点颜色BufferAttributeLinear-sRGB自动颜色贴图PNG/JPEG 的.map、.emissiveMapsRGB闭区间texture.colorSpace SRGBColorSpace浮点数据贴图OpenEXR 的.envMap、.lightMapLinear-sRGB开区间可超 1texture.colorSpace LinearSRGBColorSpace非颜色贴图.normalMap、.roughnessMap无颜色空间texture.colorSpace NoColorSpace⚠️ 警告很多 3D 模型格式没有正确、一致地标注颜色空间老格式尤其容易出问题。建议优先用glTF 2.0GLTFLoader并尽早用在线查看器验证资源本身是否正确。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.outputColorSpaceTHREE.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.rcolor.gcolor.b0.5;console.log(color.r);// → 0.5// 手动转换color.r0.5;color.convertSRGBToLinear();console.log(color.r);// → 0.214041140开启ColorManagement推荐后因为十六进制和 CSS 颜色本质是 sRGBsetter 会自动把输入从 sRGB 转成 Linear-sRGBgetter 返回十六进制/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这是铁律。总结一句话记住颜色管理的核心分工角色颜色空间关键点输入颜色贴图sRGBtexture.colorSpace SRGBColorSpace输入法线/粗糙度等无texture.colorSpace NoColorSpace工作计算Linear-sRGBThree.js 固定别手动改输出屏幕sRGBrenderer.outputColorSpace后期处理要OutputPass颜色实例Linear-sRGB开启ColorManagement后 hex/CSS 自动转实用 checklist颜色发暗→检查颜色贴图的colorSpace整屏过曝→检查后期处理有没有OutputPass模型颜色怪→优先用 glTF 2.0 并早验证。延伸阅读three.js docs – ColorManagementthree.js docs – WebGLRenderer.outputColorSpacethree.js docs – Texture.colorSpaceGPU Gems 3: The Importance of Being LinearWhat every coder should know about gammaThe Hitchhiker’s Guide to Digital Color