阅读视图

发现新文章,点击刷新页面。

Three.js 色彩空间的正确使用方式

three中色彩空间常见用处

// 给材质设置色彩空间
material1.map.colorSpace = THREE.SRGBColorSpace;

// 给渲染器的输出色彩空间, 不设置的话默认值也是SRGBColorSpace
new THREE.WebGLRenderer( { outputColorSpace: THREE.SRGBColorSpace } );

three.js r152+ 之后默认就是 SRGBColorSpace,老版本(outputEncoding 时代)行为不同

色彩空间的选项

  • SRGBColorSpace-sRGB 色彩空间

  • LinearSRGBColorSpace-线性sRGB色彩空间

区别?

SRGBColorSpace进行了伽马校正

为什么会有伽马校正?

  1. 纠正硬件的问题

在液晶显示器普及之前,使用的是笨重的 CRT (阴影 栅格 显像管 电视。CRT 的工作原理是用电子枪射出电子束轰击屏幕,科学家发现,电子枪的电压值和屏幕产生的亮度之间并不是 1:1 的线性关系,而是一个幂函数关系:

也就是如下图中红色曲线所示,跟原本蓝色虚线比较,亮度是偏低的

所以为了还原真实效果,抵消调 CRT压低的亮度,那就把真实亮度数据提高,提高成绿色曲线那样,这样一抵消,显示就正常了,这个提高的过程就是伽马校正

  1. 也能满足存储空间合理分配

  • 人眼特性:我们对暗部的变化非常敏感,而对亮部的变化比较迟钝。

  • 数据分配的矛盾:如果我们在电脑里用“线性”方式存储亮度(比如 0 代表黑,128 代表半亮,255 代表全亮):

    • 在 0 到 10 之间(暗部),只有 10 个档位。因为我们眼睛太敏感,这 10 个档位之间的跳变看起来会像阶梯一样,非常不自然(这就是“色彩断层”)。
    • 在 200 到 250 之间(亮部),虽然有 50 个档位,但我们的眼睛根本分不出这 50 种亮度的区别。这部分昂贵的存储空间(位深)就被浪费了。
  • 解决方案(伽马编码) : 故意把 256 个档位中的大部分都分给“暗部”,只留少部分给“亮部”。这样既照顾了人眼的敏感度,又没有浪费存储空间。这样我们可以在 8 位(0-255)的空间里,把更多数值分配给敏感的暗部,让有限的资源发挥最大效用。如图所示(随便找一个以前的暗部区域值,映射后占居的区域明显变多)

为什么现在屏幕“正常”了,还需要它?

现在的液晶(LCD)或 OLED 屏幕完全可以做到“给 128 就亮 50%”,为什么还要折腾?

行业标准的惯性

全球互联网上 99% 的图片(JPEG)、视频(MP4)和网页标准(HTML/CSS)都是基于 sRGB 色彩空间存储的

  • 如果显示器突然改为“线性显示”,那么所有的互联网内容看起来都会变得非常亮

  • 并且图片大多还是如图所示8位,需要上面说过的满足存储空间并合理分配

正确色彩空间处理的流程

  1. 原始图片 默认是 sRGB 色彩空间,它自带一条 “上翘” 的伽马曲线

  2. 转成 线性空间 :在进入 GPU 运算前,需要先把图片从 sRGB 非线性空间转换为 Linear(线性)sRGB 空间。这一步会把上翘的曲线 “拉平” 成一条直线,让亮度数据恢复成物理上均匀的数值,确保后续的光照、混合等计算结果是准确的。

  3. 程序运算 :在线性空间里进行渲染计算,比如光影追踪、材质混合、特效合成等。因为线性空间的亮度是均匀的,所以计算出来的光影效果才符合物理规律,不会出现颜色偏差或暗部丢失。

  4. 渲染结果 :对计算结果,经过伽马校正后,得到的就是最终的 sRGB 格式渲染结果,它的亮度曲线和原始图片的格式是一致的。

  5. 显示器显示 🖥️显示器接收到 sRGB 信号后,会用它自带的伽马曲线(通常 γ≈2.2)来显示,这个过程会把信号 “压暗”。因为我们已经提前做了伽马校正,所以两次曲线变化刚好抵消,最终显示在屏幕上的亮度就和我们计算的结果完全一致。

  6. 人眼感知 👀最终画面被人眼看到,色彩和亮度都保持了设计和计算时的真实效果,不会出现过暗或过亮的问题。

总结

也就是我们要保证用来计算的时候是 Linear( 线性空间,用来渲染的时候是sRGB 空间,那在three中如何做到?

Three.js从输入的角度

Three.js中我们只需要指定 色彩空间 类型即可,程序会帮我们转成线性,所以我们要做的就是把应该指定为SRGBColorSpace的纹理,指定为SRGBColorSpace

举几种常见加载器对加载后的图片色彩空间的处理逻辑

TextureLoader

TextureLoader 不设置 colorSpace,保持默认 NoColorSpace,需要手动设置:

注意!颜色纹理需要手动指定色彩空间为SRGBColorSpace,像下文GLTFLoader中的逻辑一样,

例如

const texture = await loader.loadAsync( 'textures/land_ocean_ice_cloud_2048.jpg' );
texture.colorSpace = THREE.SRGBColorSpace;

CubeTextureLoader

CubeTextureLoader 固定设置为 SRGBColorSpace:

GLTFLoader

只有颜色纹理会被设置为 SRGBColorSpace,其他纹理保持 NoColorSpace:

设置为 SRGBColorSpace 的纹理:

  • baseColorTexture (map) → SRGBColorSpace

  • emissiveTexture (emissiveMap) → SRGBColorSpace

  • sheenColorTexture (sheenColorMap) → SRGBColorSpace

  • specularColorTexture (specularColorMap) → SRGBColorSpace

这几种色彩空间标记的处理逻辑

exture.colorSpace 内部格式 sRGB → Linear 转换
NoColorSpace(默认) RGBA8 不转换,原样上传
SRGBColorSpace SRGB8_ALPHA8 GPU 采样时自动转 SRGB8_ALPHA8 是 WebGL 2.0 的 sRGB 纹理格式 GPU 在采样时自动应用 sRGB EOTF(Electro-Optical Transfer Function)将 sRGB 转为线性
LinearSRGBColorSpace RGBA8 不转换,已是线性

也提供了方法可以手动转化,THREE.Color 类下即可调用

src/math/ColorManagement.js

export function SRGBToLinear( c ) {

    return ( c < 0.04045 ) ? c * 0.0773993808 : Math.pow( c * 0.9478672986 + 0.0521327014, 2.4 );

}

export function LinearToSRGB( c ) {

    return ( c < 0.0031308 ) ? c * 12.92 : 1.055 * ( Math.pow( c, 0.41666 ) ) - 0.055;

}

Three.js中-从输出的角度

threejs会在

  • MeshBasicMaterial
  • MeshPhysicalMaterial
  • MeshPhongMaterial
  • MeshLambertMaterial
  • MeshToonMaterial
  • MeshMatcapMaterial
  • SpriteMaterial
  • PointsMaterial 等等

材质输出的时候增加这样一段代码

src/renderers/shaders/ShaderChunk/colorspace_fragment.glsl.js

 gl_FragColor = linearToOutputTexel( gl_FragColor );

linearToOutputTexel函数会根据outputColorSpace来动态配置

  getTexelEncodingFunction( 'linearToOutputTexel', parameters.outputColorSpace ),

说白了就是输出的时候会跟你设置的outputColorSpace来判断需不需要转成SRGBColorSpace,默认是转成SRGBColorSpace

我们自己写的ShaderMaterial输出的时候怎么办

我们也可以调用linearToOutputTexel

因为linearToOutputTexel

  • 注入时机:在 WebGLProgram 构造函数中构建 prefixFragment 时

  • 注入方式:通过 getTexelEncodingFunction 动态生成函数代码,添加到 prefixFragment

  • 可用性:所有非 RawShaderMaterial 的材质(包括 ShaderMaterial)都会自动注入

总结

在 three.js 中,默认的色彩空间配置已经覆盖了大多数使用场景。只要遵循颜色纹理使用 sRGB、渲染计算在 线性空间 、输出再转回 sRGB这一基本原则,画面通常就是正确的。但是理解色彩空间与伽马校正的原理,才能在自定义 Shader、特殊纹理或渲染需求出现时,有意识地手动调整配置,而不是盲目试参数

❌