普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月26日首页

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

作者 乘风转舵
2026年1月26日 17:44

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、特殊纹理或渲染需求出现时,有意识地手动调整配置,而不是盲目试参数

通过重新生成来修复字体文件问题

作者 乘风转舵
2026年1月26日 17:43

有没有遇到这种情况,

美术导出的字体,她用着可以,但是前端页面引用就不生效

可能的原因

1. 浏览器的“安检机制”:OTS 拦截

现代浏览器(Chrome、Firefox、Safari)在加载 Web 字体时,都会运行一个叫作 OTS (OpenType Sanitizer) 的程序。

  • 它的职责: 防止恶意字体利用解析漏洞攻击用户的系统。它会对字体的每一个二进制 Table(数据表)进行极其严格的校验。
  • 后果: 如果字体文件的校验和(Checksum)对不上,或者内部索引表有 1 字节的偏移错误,浏览器会直接拒绝加载该文件,并在控制台报错。
  • Photoshop 的做法: PS 调用的是操作系统的字体引擎(或者是 Adobe 自家的引擎)。这些引擎为了兼容老旧字体,通常非常“宽容”。即使文件结构有瑕疵,只要它能找到笔画数据,它就能强行画出来。

2. 权限与版权位(Embedding Bits)

字体文件内部有一个字段叫 fsType,专门标记该字体是否允许“嵌入”。

  • 网页端的限制: 如果这个位被设置为“受限(Restricted)”,浏览器会遵循版权协议,拒绝在网页上显示该字体。
  • Photoshop 的做法: 既然设计师能把字体装进系统,PS 就认为你已经拥有了使用权,所以它不会限制你在设计稿里使用它。
  • FontForge 的作用: 当你在 FontForge 里导出新字体时,默认设置通常会重置这些权限位,使其变为“可嵌入(Installable Embedding)”,从而解开了浏览器的枷锁。

3. 命名空间与跨域(CORS)

虽然这与字体内部结构关系较小,但也是前端常遇到的坑:

  • 文件头信息: 有些字体内部的 PostScript Name 包含特殊字符或中文字符,PS 能够识别,但 CSS 引用时如果名称不匹配或包含非法字符,浏览器就找不到。
  • FontForge 的作用: 导出过程会根据规范重新格式化字体的“名字表(Naming Table)”,消除了命名的歧义。

解决方法

使用FontForge这类字体编辑器重新生成一下字体文件来解决

重新生成会做如下的事情

  1. 清理冗余数据: FontForge 会丢弃原文件中不规范的自定义数据。
  2. 重新计算校验: 它会为所有的 Table 重新计算正确的校验和(Checksum),这直接通过了浏览器的 OTS 安检
  3. 标准化格式: 它强制按照 OpenType/TrueType 最新的标准协议来排列文件的二进制结构。

FontForge下载

  • 开源免费
  • 跨平台(Windows/macOS/Linux)
  • 支持 TTF/OTF/WOFF/WOFF2/SVG/BDF 等互转

官网 fontforge.org

使用文档 fontforge.org/docs/ui/men…

image.png

导入字体

image.png

这里可以查看字体的报错

image.png

image.png

  • Missing Points at Extrema(极值点缺失):这主要影响字体在特定尺寸下的渲染清晰度(Hinting)。

  • Self Intersecting(路径自相交):这可能导致某些软件里填充色块异常。

  • 结论: 这些属于“绘图规范”问题,它们通常不会导致字体无法加载,只会让字看起来可能有点丑或渲染不完美

重新生成字体文件

image.png

选择导出的文件格式

image.png

  • 原始文件可能存在的问题: 原始字体可能存在损坏的偏移量、错误的校验和(Checksum)、或者不符合规范的 Header(头部信息)。浏览器(尤其是 Chrome/Firefox)对 Web 字体的安全性检查非常严格,只要结构有一点不合规,就会拒绝加载。

  • FontForge 的作用: 当你点击“Generate”时,FontForge 并不是简单地“复制”旧文件,而是根据它内存中的数据模型,重新从零开始构建了一个全新的 .ttf 文件。它会自动生成符合标准的新 Table(如 head, hhea, maxp, OS/2 等)。这个“重写”过程自动修复了导致浏览器报错的底层结构问题。

导出之后新的字体文件就可以用了,对于前端来说了解到这就够用了

inspira-ui中Gradient Button效果原理

作者 乘风转舵
2026年1月26日 17:43

原效果地址

inspira-ui.com/docs/en/com…

核心原理分析

 <button
    class="animate-rainbow rainbow-btn relative flex min-h-10 min-w-28 items-center justify-center overflow-hidden before:absolute before:-inset-[200%]"
    :class="[props.class]"
  >
    <span class="btn-content inline-flex size-full items-center justify-center px-4 py-2">
      <slot / >
    </span>
  </button>

如源码所示元素就button跟span

其实分了三层,下面来解释这三层的作用

span-内容区域

用来承载内容跟背景色

button-外层按钮容器

如图所示button通过设置padding来控制并当作borderWidth

::before-背景层

colors: () => [
    "#FF0000",
    "#FFA500",
    "#FFFF00",
    "#008000",
    "#0000FF",
    "#4B0082",
    "#EE82EE",
    "#FF0000",
  ]
  
  
. animate - rainbow ::before {
  ...
  background: conic-gradient(v-bind(allColors));
  ...
}

生成渐变

css使用 conic-gradient 是“绕着中心点旋转”的渐变

默认是 正上方(12 点方向) 顺时针排列,给定颜色后自会均分,如图所示,这样就得到了渐变的背景

旋转渐变

@keyframes rotate-rainbow {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

 .animate-rainbow ::before {
  ...
   animation: rotate-rainbow v-bind(durationInMilliseconds) linear infinite;
}

给before这个伪类增加动画,360度无限旋转即可,如代码所示

其他小细节
 <button
    class="...before:-inset-[200%]"
    :class="[props.class]"
  >
.animate-rainbow::before {
  ...
  filter: blur(v-bind(blurPx));
  ....
}

通过inset: -200%,将 ::before 伪元素在四个方向各扩展出去200%,使其尺寸远大于按钮本身,保证旋转的时候每个位置都能覆盖按钮区域

通过filter: blur():模糊效果,使得尤其是颜色交界处没那么锐利

最后

配合button按钮的overflow-hidden 来裁剪掉外部的区域

效果就完成了

❌
❌