概要
Three.js(React Three Fiber)で、Post-processingを使うと配置したオブジェクトの色味が変わってしまうことがあります。
この現象に関して、調べたことをまとめました。
下図は、左がPost-processingなしのシーンで、右がPost-processingありのシーンです。
明らかに2つのBoxの色味が違うことがわかります。
結論から言うと、
このように色味が異なってしまうのは、Post-processingではRenderTargetがメインのものとは異なるため、ガンマ補正がのっていないからです。
先に、【まとめ】を見て頂くのがいいと思います。
ガンマ補正
そもそも**ガンマ補正ってなんだろう?**と思われるはずです。
わたしも詳しくは理解できていないのですが、以下の補正のことを言います。
ディスプレイにRGBの強さをそのまま渡すと、RGBの値は線形に出力されずに、中央の値が落ち込んだカーブで出力されて暗めの結果になります。
このディスプレイが実際に出力する色のカーブの形をディスプレイの特性とも呼びます。
ディスプレイのRGBの出力を線形に近づけるために補正が「ガンマ補正」です。
要約すると、
実際の色をそのままディスプレイに表示しようとすると、(ディスプレイの特性によって)色が暗くなるので、それを補正する必要があります。それがガンマ補正です。

React Three Fiberでは、Canvasを作成した段階でメインのRenderTargetに、このガンマ補正(γ=2.2 ≒ sRGB補正)がデフォルトで割り当てられます。
もう少し詳しくみると、Canvasのlinearプロパティがfalse(default)
の場合このガンマ補正がされて、true
の場合は補正なしになります。
Post-processingでは、RenderTargetがそもそも異なるためこの補正が効きません。
このため、冒頭のPost-processingを適用したBoxでは、色味が暗くなっています。(BackgroundやFogの色は、Materialの色とは扱いが異なるようです)
Boxの色味だけ変わるのは、おそらく色がデコードはされず、MaterialのFragmentShader内でエンコードだけされているためだと思います。
以下のテキストを見ると、Post-processingを使う場合は、ガンマ補正をするPassを最後に追加してくださいと書いてあるので、これに従います。
Post-processingの補正
Three.jsから、GammaCorrectionShaderというShaderが提供されているので、Post-processingの最後に追加します。
export const Effects: VFC = () => {
//・・・
return (
<effectComposer ref={composerRef} args={[gl]}>
<renderPass attachArray="passes" args={[scene, camera]} />
<CustomPass enabled={datas.CustomPass} />
<shaderPass attachArray="passes" args={[GammaCorrectionShader]} enabled={datas.GammaCorrection} />
</effectComposer>
)
}
※ コードの全体は、【Sandbox】を参照してください。

実際に、Boxに使われているMeshStandardMaterialをみると、Shader内でエンコーディングされていることがわかります。
#include <encodings_fragment>
ちなみに、以下のように記述すると、ビルトインMaterialのShaderを覗くことができます。
const material = new THREE.MeshStandardMaterial()
material.onBeforeCompile = shader => {
console.log(shader.vertexShader)
console.log(shader.fragmentShader)
}
Three.jsのColorオブジェクトには、色空間をコンバートするメソッドがあります。今回の場合、エンコードをしたいのでconvertGammaToLinear()
またはconvertSRGBToLinear()
を使用します。
useEffect(() => {
const color = new THREE.Color(datas.color)
switch (datas.Encoding) {
case 'Gamma':
color.convertGammaToLinear()
break
case 'sRGB':
color.convertSRGBToLinear()
break
default:
break
}
// set background
scene.background = color
}, [datas, scene])
Three.jsのγ値のデフォルトはγ=2.0
で、sRGB補正だとγ=2.2
相当のガンマ補正になります。
GammaCorrectionShaderの中身を見ると、LinearTosRGB
となっているので、convertSRGBToLinear()
を使うと元の色と同じになります。(厳密には微小な誤差が発生しますが...)
gl_FragColor = LinearTosRGB( tex );
Textureの補正
Textureは、RenderTargetにガンマ補正(デコーダー)の設定をしていても、エンコーダーが自動で設定されず、何も設定しないとデフォルト値のLinearEncording
となります。
このため、デフォルトの状態だとTextureは全体的に白っぽい色合いになります。
下図は、左がリソース画像、右がBackgroundに設定したTextureです。
TextureにsRGBEncoding
を割り当てることで、リソース画像の色合いと同じになります。
const { size, scene } = useThree()
const texture = useTexture('/assets/image.jpg')
texture.needsUpdate = true
// set background
scene.background = texture
useEffect(() => {
switch (datas.Encoding) {
case 'Gamma':
texture.encoding = THREE.GammaEncoding
break
case 'sRGB':
texture.encoding = THREE.sRGBEncoding
break
default:
texture.encoding = THREE.LinearEncoding
}
}, [datas, texture])

もちろん背景にTextureを取った場合でも、Post-processingを使う場合は、GammaCorrectionShader
を追加する必要があります。
Post-processingとShaderMaterialを併用する
追記(2022.02.28)
Three r138 では、sRGBToLinearは使用できなくなっています。
textureに関しては、【Textureの補正】同様に、texture.encoding = THREE.sRGBEncodingを設定することで、補正が効くようになっています。
通常の色の扱いについては調査中です。
Post-processingとShaderMaterialを併用する場合、ShaderMaterialのgl_FragColorに渡す色情報をエンコードする必要があります。
Post-processingのGammaCorrectionShaderでは、LinearTosRGB
でデコードされているので、逆のことをしてあげればOKです。
uniform vec3 u_color;
void main() {
vec4 color = vec4(u_color, 1.0);
gl_FragColor = sRGBToLinear(color);
}
sRGBToLinear
は、Three.jsのShaderChunkに含まれているので、参照などを追加しないで利用することができます。
encodings_pars_fragment.glsl.js
FragmentShaderで、Textureを扱う場合も同様です。
Sandbox
using built-in material
using shader material
まとめ
結構手順が多くて、わりと面倒くさいです。
なのでまず、Canvasのプロパティlinear = true
(ガンマ補正をしない)を試して、色味が特に気にならなければそれでいいと思います。
というか、バニラのThree.jsだとWebGLRenderer.outputEncoding = THREE.LinearEncoding
がデフォルトになってるんですね。