| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- <!DOCTYPE html><html lang="zh"><head>
- <meta charset="utf-8">
- <title>颜色管理</title>
- <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
- <meta name="twitter:card" content="summary_large_image">
- <meta name="twitter:site" content="@threejs">
- <meta name="twitter:title" content="Three.js - 颜色管理">
- <meta property="og:image" content="https://threejs.org/files/share.png">
- <link rel="shortcut icon" href="../../files/favicon_white.ico" media="(prefers-color-scheme: dark)">
- <link rel="shortcut icon" href="../../files/favicon.ico" media="(prefers-color-scheme: light)">
- <link rel="stylesheet" href="../resources/lesson.css">
- <link rel="stylesheet" href="../resources/lang.css">
- <link rel="stylesheet" href="/manual/zh/lang.css">
- <script type="importmap">
- {
- "imports": {
- "three": "../../build/three.module.js"
- }
- }
- </script>
- <style>
- blockquote {
- font-size: 0.8em;
- line-height: 1.5em;
- margin-left: 0;
- border-left: 4px solid #cccccc;
- padding: 1em 2em 1em 2em;
- }
- blockquote p:first-child {
- margin-top: 0;
- }
- blockquote p:last-child {
- margin-bottom: 0;
- }
- figure {
- width: 100%;
- margin: 1em 0;
- font-style: italic;
- }
- figure img {
- width: 100%;
- }
- figure.float {
- float: right;
- max-width: 30%;
- margin: 1em;
- }
- @media all and ( max-width: 640px ) {
- figure.float {
- float: none;
- max-width: 100%;
- }
- }
- </style>
- </head>
- <body>
- <div class="container">
- <div class="lesson-title">
- <h1>颜色管理</h1>
- </div>
- <div class="lesson">
- <div class="lesson-main">
-
- <h2>什么是色彩空间?</h2>
- <p>
- 每一种色彩空间,都是一组经过权衡的设计选择。它们共同目标是:
- 在满足精度和显示技术限制的前提下,覆盖尽可能大的颜色范围。
- 在创建 3D 资源或把多个 3D 资源组装进同一场景时,
- 理解这些属性及其在不同色彩空间之间的关系非常重要。
- </p>
- <figure class="float">
- <img src="../resources/srgb_gamut.png" alt="">
- <figcaption>
- 参考 CIE 1931 色度图中的 sRGB 颜色与白点(D65)。
- 彩色区域是 sRGB 色域(三维体积)在二维中的投影。
- 来源:<a href="https://en.wikipedia.org/wiki/SRGB" target="_blank" rel="noopener">Wikipedia</a>
- </figcaption>
- </figure>
- <ul>
- <li>
- <b>色彩原色(Color primaries):</b>原色(如红、绿、蓝)并非绝对值;
- 它们是在有限精度与显示设备能力约束下,从可见光谱中选定的。
- 颜色由各原色的比例来表达。
- </li>
- <li>
- <b>白点(White point):</b>大多数色彩空间都会定义:
- 当原色满足 <i>R = G = B</i> 时呈现“无色(中性)”。
- 白、灰等中性色的视觉效果依赖人眼感知,而感知又与观察环境相关。
- 因此色彩空间会指定一个“白点”以统一基准。
- sRGB 的白点是 [link:https://en.wikipedia.org/wiki/Illuminant_D65 D65]。
- </li>
- <li>
- <b>传递函数(Transfer functions):</b>在确定色域和颜色模型后,
- 还要定义数值与颜色空间之间的映射(传递函数)。
- <i>r = 0.5</i> 是表示物理光照比 <i>r = 1.0</i> 少 50%,
- 还是表示人眼感知亮度少 50%?两者并不等价,
- 差异由数学函数来描述。根据目标不同,传递函数可以是
- <i>线性</i>或<i>非线性</i>。sRGB 使用非线性传递函数。
- 这些函数有时会近似为<i>伽马函数</i>,但“gamma”一词在这里含义模糊,
- 应尽量避免混用。
- </li>
- </ul>
- 以上三个参数(原色、白点、传递函数)共同定义了一个色彩空间。
- 在此基础上,再补充几个术语会更清晰:
- <ul>
- <li>
- <b>颜色模型(Color model):</b>在既定色域中用数字描述颜色的方式,
- 可理解为颜色坐标系。在 three.js 里我们主要使用 RGB 模型,
- 坐标为 <i>r, g, b ∈ [0,1]</i>(闭区间)或
- <i>r, g, b ∈ [0,+∞)</i>(开域),每一项都表示某个原色所占比例。
- 其他模型(HSL、Lab、LCH)常用于美术调色。
- </li>
- <li>
- <b>色域(Color gamut):</b>当原色与白点确定后,就确定了可见光谱中的一个体积范围,
- 这就是“色域”。不在这个体积内的颜色(超出色域)无法用闭区间 [0,1] 的 RGB 表达。
- 在开域 [0,+∞) 中,色域在数学上可以视为无限。
- </li>
- </ul>
- <p>
- 来看两个最常见的色彩空间:`SRGBColorSpace`(sRGB)与
- `LinearSRGBColorSpace`(Linear-sRGB)。两者原色和白点相同,
- 因此色域一致,也都使用 RGB 模型。它们只在传递函数上不同:
- Linear-sRGB 相对于物理光强是线性的;sRGB 使用非线性传递函数,
- 更接近人眼感知与常见显示设备的响应特性。
- </p>
- <p>
- 这个差异非常关键。光照计算和大多数渲染运算通常必须在线性色彩空间中完成。
- 但线性颜色在图像或帧缓冲中的存储效率较低,且直接显示给人眼时观感不正确。
- 因此,输入纹理与最终输出图像通常会使用非线性的 sRGB 色彩空间。
- </p>
- <blockquote>
- <p>
- ℹ️ <i><b>注意:</b>虽然部分现代显示器支持 Display-P3 等更宽色域,
- 但 Web 平台图形 API 仍主要基于 sRGB。
- 当前 three.js 应用通常只会使用 sRGB 与 Linear-sRGB。</i>
- </p>
- </blockquote>
- <h2>色彩空间在流程中的角色</h2>
- <p>
- 现代渲染所需的线性工作流通常会涉及不止一种色彩空间,
- 每种色彩空间承担不同职责。线性与非线性色彩空间适用于不同环节,
- 如下所示。
- </p>
- <h3>输入色彩空间</h3>
- <p>
- 传入 three.js 的颜色(来自取色器、纹理、3D 模型等)都带有各自色彩空间。
- 凡是不在 Linear-sRGB 工作色彩空间中的输入,都需要转换;
- 纹理也必须正确设置 <i>texture.colorSpace</i>。
- 如果在初始化颜色前启用 THREE.ColorManagement,
- 某些转换(如十六进制颜色与 CSS sRGB 颜色)会自动处理:
- </p>
- <code>
- THREE.ColorManagement.enabled = true;
- </code>
- <p>
- THREE.ColorManagement 默认已启用。
- </p>
- <ul>
- <li>
- <b>材质、灯光与着色器:</b>其颜色数据中的 RGB 分量存储在线性的
- Linear-sRGB 工作空间中。
- </li>
- <li>
- <b>顶点颜色:</b>`BufferAttribute` 中的 RGB 分量也存储在
- Linear-sRGB 工作空间中。
- </li>
- <li>
- <b>颜色纹理:</b>包含颜色信息的 PNG/JPEG `Texture`
- (如 `.map`、`.emissiveMap`)应使用闭区间 sRGB,
- 并标注 <i>texture.colorSpace = SRGBColorSpace</i>。
- OpenEXR 等格式(常用于 `.envMap`、`.lightMap`)则使用 Linear-sRGB,
- 标注为 <i>texture.colorSpace = LinearSRGBColorSpace</i>,
- 并且可能包含开域 [0,+∞) 的值。
- </li>
- <li>
- <b>非颜色纹理:</b>不存储颜色信息的纹理(如 `.normalMap`、`.roughnessMap`)
- 没有对应色彩空间,一般使用默认标注
- <i>texture.colorSpace = NoColorSpace</i>。
- 在少数场景下,非颜色数据可能因技术原因采用其他非线性编码。
- </li>
- </ul>
- <blockquote>
- <p>
- ⚠️ <i><b>警告:</b>许多 3D 模型格式并未正确或一致地定义色彩空间信息。
- three.js 虽会尽量处理常见情况,但旧格式仍常出现问题。
- 为获得最佳结果,请优先使用 glTF 2.0(`GLTFLoader`),
- 并尽早在在线查看器中验证资源本身是否正确。</i>
- </p>
- </blockquote>
- <h3>工作色彩空间</h3>
- <p>
- 渲染、插值及许多其他计算,必须在开域的线性工作色彩空间中进行,
- 此时 RGB 分量与物理光照强度成比例。在 three.js 中,
- 工作色彩空间是 Linear-sRGB。
- </p>
- <h3>输出色彩空间</h3>
- <p>
- 输出到显示设备、图片或视频时,通常需要将开域 Linear-sRGB
- 工作空间转换到目标色彩空间。该转换由
- `WebGLRenderer.outputColorSpace` 定义。
- 使用后处理时,需要 `OutputPass`。
- </p>
- <ul>
- <li>
- <b>显示:</b>写入 WebGL 画布并显示的颜色应使用 sRGB。
- </li>
- <li>
- <b>图像:</b>写入图像时应使用与格式和用途匹配的色彩空间。
- 完整渲染后保存为 PNG/JPEG 的图片通常使用 sRGB。
- 若图像包含自发光、光照贴图或其他不受 [0,1] 限制的数据,
- 通常使用开域 Linear-sRGB,并配合 OpenEXR 等兼容格式。
- </li>
- </ul>
- <blockquote>
- <p>
- ⚠️ <i><b>警告:</b>渲染目标可使用 sRGB 或 Linear-sRGB。
- sRGB 在有限精度下利用率更高:在闭区间内,sRGB 常用 8-bit 即可,
- 而 Linear-sRGB 可能需要至少 16-bit(half float)。
- 若后续管线阶段还要求 Linear-sRGB 输入,额外转换会带来一定性能开销。</i>
- </p>
- </blockquote>
- <p>
- 基于 `ShaderMaterial` 和 `RawShaderMaterial` 的自定义材质需要自行实现输出色彩空间转换。
- 对于 `ShaderMaterial`,通常在片元着色器 `main()` 中加入
- `colorspace_fragment` shader chunk 即可。
- </p>
- <h2>使用 THREE.Color 实例</h2>
- <p>
- 读取或修改 `Color` 的方法默认假设数据已经在 three.js 的工作色彩空间
- (Linear-sRGB)中。RGB 与 HSL 分量都直接对应 `Color` 实例内部数据,
- 不会被隐式转换。你可以显式调用
- <i>.convertLinearToSRGB()</i> 或 <i>.convertSRGBToLinear()</i> 进行转换。
- </p>
- <pre class="prettyprint notranslate lang-js" translate="no">
- // 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
- </pre>
- <p>
- 当设置 <i>ColorManagement.enabled = true</i>(推荐,且默认开启)后,
- 某些转换会自动执行。由于十六进制与 CSS 颜色通常属于 sRGB,
- `Color` 在 setter 中会把它们从 sRGB 转为 Linear-sRGB;
- 在 getter 返回十六进制或 CSS 值时,则会从 Linear-sRGB 转回 sRGB。
- </p>
- <pre class="prettyprint notranslate lang-js" translate="no">
- // 十六进制转换。
- color.setHex( 0x808080 );
- console.log( color.r ); // → 0.214041140
- console.log( color.getHex() ); // → 0x808080
- // CSS 颜色转换。
- color.setStyle( 'rgb( 0.5, 0.5, 0.5 )' );
- console.log( color.r ); // → 0.214041140
- // 通过 'colorSpace' 参数覆盖默认转换。
- color.setHex( 0x808080, LinearSRGBColorSpace );
- console.log( color.r ); // → 0.5
- console.log( color.getHex( LinearSRGBColorSpace ) ); // → 0x808080
- console.log( color.getHex( SRGBColorSpace ) ); // → 0xBCBCBC
- </pre>
- <h2>常见错误</h2>
- <p>
- 当某个颜色或纹理配置错误时,它看起来会比预期更亮或更暗。
- 当渲染器输出色彩空间配置错误时,整张场景都可能偏暗
- (例如遗漏了到 sRGB 的转换)或偏亮(例如后处理中重复转换到 sRGB)。
- 这类问题通常并非全局线性偏差,单纯增减光照并不能真正解决。
- </p>
- <p>
- 更隐蔽的问题是:当输入和输出色彩空间<i>都</i>设置错误时,
- 整体亮度看似正常,但颜色会在不同光照下异常变化,
- 或明暗层次变得过曝、生硬。两个错误不会相互抵消。
- 务必确保工作色彩空间是线性的(scene referred),
- 输出色彩空间是非线性的(display referred)。
- </p>
- <h2>延伸阅读</h2>
- <ul>
- <li>
- <a href="https://developer.nvidia.com/gpugems/gpugems3/part-iv-image-effects/chapter-24-importance-being-linear" target="_blank" rel="noopener">GPU Gems 3: The Importance of Being Linear</a>, by Larry Gritz and Eugene d'Eon
- </li>
- <li>
- <a href="https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/" target="_blank" rel="noopener">What every coder should know about gamma</a>, by John Novak
- </li>
- <li>
- <a href="https://hg2dc.com/" target="_blank" rel="noopener">The Hitchhiker's Guide to Digital Color</a>, by Troy Sobotka
- </li>
- <li>
- <a href="https://docs.blender.org/manual/en/latest/render/color_management.html" target="_blank" rel="noopener">Color Management</a>, Blender
- </li>
- </ul>
- </div>
- </div>
- </div>
- <script src="../resources/prettify.js"></script>
- <script src="../resources/lesson.js"></script>
- </body></html>
|