【React Three Fiber】Post-processingとガンマ補正の関係について

<!-- ========================================================= -->
# 概要
<!-- ========================================================= -->
Three.js（React Three Fiber）で、Post-processingを使うと配置したオブジェクトの色味が変わってしまうことがあります。
この現象に関して、調べたことをまとめました。

下図は、左がPost-processingなしのシーンで、右がPost-processingありのシーンです。
![スクリーンショット 2022-01-15 184230.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/69d1b076-5564-912d-fa52-4385f816a742.png)
明らかに2つのBoxの色味が違うことがわかります。

:::note info
結論から言うと、
このように色味が異なってしまうのは、Post-processingではRenderTargetがメインのものとは異なるため、ガンマ補正がのっていないからです。
:::

:::note info
先に、【まとめ】を見て頂くのがいいと思います。
:::

<!-- ========================================================= -->
# ガンマ補正
<!-- ========================================================= -->
そもそも**ガンマ補正ってなんだろう？**と思われるはずです。
わたしも詳しくは理解できていないのですが、以下の補正のことを言います。

>ディスプレイにRGBの強さをそのまま渡すと、RGBの値は線形に出力されずに、中央の値が落ち込んだカーブで出力されて暗めの結果になります。
>このディスプレイが実際に出力する色のカーブの形をディスプレイの特性とも呼びます。
>ディスプレイのRGBの出力を線形に近づけるために補正が「ガンマ補正」です。

>[three.jsの組み込みuniform/attributeの紹介](https://qiita.com/gam0022/items/1fe17e93f0cd0432a8b0#threejs-r82%E3%81%AE%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BFuniformattribute)

要約すると、
実際の色をそのままディスプレイに表示しようとすると、（ディスプレイの特性によって）色が暗くなるので、それを補正する必要があります。それがガンマ補正です。

<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/334819c1-ffd6-27f9-24bb-c20dc2be71e3.png' width='300'>

React Three Fiberでは、Canvasを作成した段階でメインのRenderTargetに、このガンマ補正（γ=2.2 ≒ sRGB補正）がデフォルトで割り当てられます。

もう少し詳しくみると、Canvasの**linear**プロパティが`false（default）`の場合このガンマ補正がされて、`true`の場合は補正なしになります。

https://docs.pmnd.rs/react-three-fiber/api/canvas

**Post-processingでは、RenderTargetがそもそも異なるためこの補正が効きません。**
このため、冒頭のPost-processingを適用したBoxでは、色味が暗くなっています。（BackgroundやFogの色は、Materialの色とは扱いが異なるようです）

Boxの色味だけ変わるのは、おそらく色がデコードはされず、MaterialのFragmentShader内でエンコードだけされているためだと思います。


以下のテキストを見ると、Post-processingを使う場合は、**ガンマ補正をするPassを最後に追加してください**と書いてあるので、これに従います。

https://www.donmccurdy.com/2020/06/17/color-management-in-threejs

<!-- ========================================================= -->
# Post-processingの補正
<!-- ========================================================= -->
Three.jsから、[**GammaCorrectionShader**](https://github.com/mrdoob/three.js/blob/812753b19053a2f952a5d5da7b8047c6c2be4b0c/examples/jsm/shaders/GammaCorrectionShader.js)というShaderが提供されているので、Post-processingの最後に追加します。

```.tsx:Effects.tsx
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】を参照してください。

<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/bad49461-4793-5a0a-b318-5352565aa6c6.png' width='600'>
Boxの色は、Post-processingなしと一致しました。ただ、今度は背景色が薄くなってしまいました。
これは、GammaCorrectionShaderがデコーダーで、背景色にはガンマ補正のエンコーディングがされていない（線形空間にある）ためです。

実際に、Boxに使われているMeshStandardMaterialをみると、Shader内でエンコーディングされていることがわかります。

```.glsl
#include <encodings_fragment>
```
[encodings_fragment.glsl.js](https://github.com/mrdoob/three.js/blob/master/src/renderers/shaders/ShaderChunk/encodings_fragment.glsl.js)

ちなみに、以下のように記述すると、ビルトインMaterialのShaderを覗くことができます。

```.ts
const material = new THREE.MeshStandardMaterial()
material.onBeforeCompile = shader => {
	console.log(shader.vertexShader)
	console.log(shader.fragmentShader)
}
```

Three.jsの[Colorオブジェクト](https://threejs.org/docs/#api/en/math/Color)には、色空間をコンバートするメソッドがあります。今回の場合、エンコードをしたいので`convertGammaToLinear()`または`convertSRGBToLinear()`を使用します。

```.tsx:Background.tsx
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](https://github.com/mrdoob/three.js/blob/812753b19053a2f952a5d5da7b8047c6c2be4b0c/examples/jsm/shaders/GammaCorrectionShader.js)の中身を見ると、`LinearTosRGB`となっているので、`convertSRGBToLinear()`を使うと元の色と同じになります。（厳密には微小な誤差が発生しますが...）

```.glsl:GammaCorrectionShader.js
gl_FragColor = LinearTosRGB( tex );
```

![スクリーンショット 2022-01-15 184230 - コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/aa0e4248-407c-b476-1742-35afd492495f.png)

<!-- ========================================================= -->
# Textureの補正
<!-- ========================================================= -->
Textureは、RenderTargetにガンマ補正（デコーダー）の設定をしていても、エンコーダーが自動で設定されず、何も設定しないとデフォルト値の`LinearEncording`となります。
このため、デフォルトの状態だとTextureは全体的に白っぽい色合いになります。

下図は、左がリソース画像、右がBackgroundに設定したTextureです。
![スクリーンショット 2022-01-15 205300.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/b07c3a11-f4d6-1531-b16e-8a480d95e25d.png)
Textureに`sRGBEncoding`を割り当てることで、リソース画像の色合いと同じになります。

```.tsx:Background.tsx
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])
```
<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/9512ee9b-256b-7a50-b937-e00d03da7ddb.png' width='600'>

もちろん背景にTextureを取った場合でも、Post-processingを使う場合は、`GammaCorrectionShader`を追加する必要があります。
![スクリーンショット 2022-01-15 211237.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/50faa3c8-5bab-0cf7-62f3-a06e31e35f9b.png)

<!-- ========================================================= -->
# Post-processingとShaderMaterialを併用する
<!-- ========================================================= -->
:::note info
<font color='red'>追記（2022.02.28）</font>
Three r138 では、sRGBToLinearは使用できなくなっています。
textureに関しては、【Textureの補正】同様に、<strong>texture.encoding = THREE.sRGBEncoding</strong>を設定することで、補正が効くようになっています。
通常の色の扱いについては調査中です。
:::

Post-processingとShaderMaterialを併用する場合、ShaderMaterialの**gl_FragColorに渡す色情報をエンコードする**必要があります。
Post-processingの[GammaCorrectionShader](https://github.com/mrdoob/three.js/blob/812753b19053a2f952a5d5da7b8047c6c2be4b0c/examples/jsm/shaders/GammaCorrectionShader.js)では、`LinearTosRGB`でデコードされているので、逆のことをしてあげればOKです。

```.glsl:FragmentShader
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](https://github.com/mrdoob/three.js/blob/master/src/renderers/shaders/ShaderChunk/encodings_pars_fragment.glsl.js)

FragmentShaderで、**Texture**を扱う場合も同様です。

<!-- ========================================================= -->
# Sandbox
<!-- ========================================================= -->

**using built-in material**

https://codesandbox.io/s/relationship-between-postprocessing-and-gamma-correction-with-r3f-fo83b

**using shader material**

https://codesandbox.io/s/relationship-between-postprocessing-and-gamma-correction-2-using-shadermaterial-with-r3f-y0cow

<!-- ========================================================= -->
# まとめ
<!-- ========================================================= -->
結構手順が多くて、わりと面倒くさいです。

なのでまず、Canvasのプロパティ`linear = true`（ガンマ補正をしない）を試して、色味が特に気にならなければそれでいいと思います。

というか、バニラのThree.jsだと[`WebGLRenderer.outputEncoding = THREE.LinearEncoding`](https://threejs.org/docs/#api/en/renderers/WebGLRenderer.outputEncoding)がデフォルトになってるんですね。:rolling_eyes:

<!-- ========================================================= -->
# 参照
<!-- ========================================================= -->
* [React Three Fiber - Canvas](https://docs.pmnd.rs/react-three-fiber/api/canvas)
* [Three.js - Color](https://threejs.org/docs/#api/en/math/Color)
* [Color management in three.js](https://www.donmccurdy.com/2020/06/17/color-management-in-threejs)
* [sRGBEncoding with post-processing](https://discourse.threejs.org/t/srgbencoding-with-post-processing/12582)
* [three.jsの組み込みuniform/attributeの紹介](https://qiita.com/gam0022/items/1fe17e93f0cd0432a8b0)
* [ガンマ2.2とsRGBの違い](https://knowledge.shade3d.jp/knowledgebase/%E3%82%AC%E3%83%B3%E3%83%9E2-2%E3%81%A8srgb%E3%81%AE%E9%81%95%E3%81%84)