修正postprocessing导致的颜色空间问题
# 时间紧急可以直接跳到最后面看
我是个大傻逼!!!!!
# 问题背景
问题还是出现在公司的三维项目上,领导想要这样一个功能,希望布在模型上的小方块能够在数据异常的时候闪烁颜色。threejs
中有一个比较符合的后期处理可以用,我使用的是gltf模型 + OutlinePass(描边)
+SSAARenderPass(抗锯齿)
。想着应该没问题,于是动手实现以下
使用后期处理前: 使用后期处理后: 是在发光了没错,但是为什么整个画面暗了这么多?
这里放一下我闪烁的代码:
//.....
// postprocessing
composer = new EffectComposer( renderer );
var renderPass = new RenderPass( scene, camera );
composer.addPass( renderPass );
let ssaaRenderPass = new SSAARenderPass(scene, camera);
ssaaRenderPass.unbiased = false;
ssaaRenderPass.sampleLevel = 2;
composer.addPass( ssaaRenderPass );
var params = {
edgeStrength: 4,
edgeGlow: 1,
edgeThickness: 4,
pulsePeriod: 2,
visibleEdgeColor:'#ffdf2a'
};
outlinePass = new OutlinePass( new THREE.Vector2( domEl.clientWidth, domEl.clientHeight ), scene, camera );
outlinePass.edgeStrength = params.edgeStrength;
outlinePass.edgeGlow = params.edgeGlow;
outlinePass.edgeThickness = params.edgeThickness;
outlinePass.pulsePeriod = params.pulsePeriod;
outlinePass.visibleEdgeColor.set( params.visibleEdgeColor );
composer.addPass( outlinePass );
//.....
//然后render的代码改一下
render(){
this.animateId = requestAnimationFrame(this.render)
camera.updateMatrixWorld();
//使用composer渲染
composer.render();
// renderer.render(scene, camera)
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 问题分析
在寻找解决方法前首先要了解一下色彩空间
# 色彩空间
我们来看看上图这两个灰度条,第一个是线性的从黑到白,第二个是以人类感知为准的灰度条,当人类18%左右的亮度的光源时,就能感觉到这是50%的亮度了。这就是为什么要有不同的色彩空间。
先了解一下这几个术语:
linear
颜色空间:物理上的线性颜色空间,当计算机需要对sRGB
像素运行图像处理算法时,一般会采用线性颜色空间计算。sRGB
颜色空间:sRGB
是当今一般电子设备及互联网图像上的标准颜色空间。较适应人眼的感光。sRGB
的gamma
与2.2的标准gamma
非常相似,所以在从linear转换为sRGB
时可通过转换为gamma2.2
替代。gamma
转换:线性与非线性颜色空间的转换可通过gamma
空间进行转换。
在着色器中色值的提取与色彩的计算操作一般都是在线性空间。在webgl
中,贴图或者颜色以srgb
传入时,必须转换为线性空间。计算完输出后再将线性空间转为srgb
空间。
# ThreeJS 色彩空间转换
故在ThreeJS
中,当我们为材质单独设置贴图和颜色时,需要进行色彩空间转换。具体的转换threejs
会在着色器中进行,我们只需要关注为贴图指定好色彩空间,或者直接调用转换函数。
具体步骤如下:
1.sRGB
转Linear
- 对于贴图:
threejs
需要在线性颜色空间(linear colorspace)里渲染模型的材质,而从一般软件中导出的模型中包含颜色信息的贴图一般都是sRGB
颜色空间(sRGB colorspace),故需要先将sRGB
转换为Linear。
然而 threejs
在导入材质时,会默认将贴图编码格式定义为Three.LinearEncoding
,故需将带颜色信息的贴图(baseColorTexture, emissiveTexture, 和 specularGlossinessTexture)手动指定为Three.sRGBEncoding,threejs在渲染时判断贴图为sRGB
后,会自动将贴图转换为Linear
再进行渲染计算。
// 设置加载texture的encoding
const loadTex = (callback) => {
const textureLoader = new THREE.TextureLoader();
textureLoader.load( "./assets/texture/tv-processed0.png", function(texture){
texture.encoding = THREE.sRGBEncoding;
});
...
}
2
3
4
5
6
7
8
- 对于color:
在直接定义 threejs material 的 color 值时,需要进行如下的转换:
const material = new THREE.MeshPhongMaterial( {
color: 0xBBBBBB
} );
material.color.convertSRGBToLinear();
2
3
4
- linear转gamma2.2
渲染计算后的模型仍在linear空间,展示到屏幕时需要通过gamma校正,将linear转换回sRGB空间,也就是进行gamma校正,threejs中可通过设置gammaOutput和gammaFactor,进行gamma校正,校正后的gamma2.2颜色空间与sRGB相似。
// 定义gammaOutput和gammaFactor
renderer.gammaOutput = true;
renderer.gammaFactor = 2.2; //电脑显示屏的gammaFactor为2.2
2
3
或者:
renderer.outputEncoding = THREE.sRGBEncoding;
需要注意的是:
若采用 GLTFLoader 导入带贴图的模型,GLTFLoader 将在渲染前自动把贴图设置为 THREE.sRGBEncoding,故不需要手动设置贴图 encoding。在 GLTFLoader 之前,threejs 也没有很好地处理色彩空间这回事,所以大家需要排查一下其他 loader 有没有这个 bug。
使用不受光照影响的材质,例如 MeshBasicMaterial,着色器不需要做复杂的计算,故不需要进行色彩空间转换。
# 分析原因
看完上面那样一堆啰嗦的不知道大家明白了没,为什么gltf模型会变得辣么暗呢?因为gltf默认是sRGB,然后renderer是做过修正的:
renderer.outputEncoding = THREE.sRGBEncoding;
但是使用了后期处理之后,这个修正莫名的失效了。所以究其根本是要在SSAARenderPass
和OutlinePass
上作文章,把outputEncoding改回来。
# 解决问题
我解决问题的方法可能不是最简单实用的,不过又不是不能用
OutlinePass
将OutlinePass.js
复制一份出来,然后修改代码:
if ( this.renderToScreen ) {
this.fsQuad.material = this.materialCopy;
//修改开始 同步renderer的颜色空间 添加代码
readBuffer.texture.encoding = renderer.outputEncoding;
this.copyUniforms[ "tDiffuse" ].value = readBuffer.texture;
renderer.setRenderTarget( null );
this.fsQuad.render( renderer );
}
2
3
4
5
6
7
8
9
10
2.SSAARenderPass
将SSAARenderPass.js
复制一份出来,然后修改代码
if ( ! this.sampleRenderTarget ) {
this.sampleRenderTarget = new WebGLRenderTarget( readBuffer.width, readBuffer.height, { minFilter: LinearFilter, magFilter: LinearFilter, format: RGBAFormat } );
this.sampleRenderTarget.texture.name = "SSAARenderPass.sample";
this.sampleRenderTarget.texture.encoding = renderer.outputEncoding;
}
2
3
4
5
6
7
然后看下效果: 颜色正常了~
# 写在2021-09-17,新的解决思路
因为出问题的是gltf格式的模型,原因是gltf格式模型的map
的encoding
都是THREE.sRGBEncoding
,所以改个屁源码啊!直接把gltf格式的encoding
改掉啊!!!
let loader = new GLTFLoader();/*实例化加载器*/
loader.crossOrigin = 'anonymous';
loader.setCrossOrigin('anonymous');
loader.setResourcePath(source);
loader.load(url, ( gltf ) => {
let object = gltf.scene
object.traverse(item => {
if(item.type === 'Mesh') {
item.material && item.material.map && ( item.material.map.encoding = THREE.LinearEncoding )
}
})
});
2
3
4
5
6
7
8
9
10
11
12
13
# 写在2022-12-06,最新的解决思路
使用threejs的GammaCorrectionShader
即可:
import { GammaCorrectionShader } from 'three/examples/jsm/shaders/GammaCorrectionShader.js';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'
const effectComposer = new EffectComposer(renderer)
const gammaCorrectionShader = new ShaderPass( GammaCorrectionShader );
effectComposer.addPass( gammaCorrectionShader );
2
3
4
5
6
7
8