3
Help us understand the problem. What are the problem?

posted at

updated at

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

概要

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

下図は、左がPost-processingなしのシーンで、右がPost-processingありのシーンです。
スクリーンショット 2022-01-15 184230.png
明らかに2つのBoxの色味が違うことがわかります。

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

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

ガンマ補正

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

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

three.jsの組み込みuniform/attributeの紹介

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

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の最後に追加します。

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】を参照してください。

Boxの色は、Post-processingなしと一致しました。ただ、今度は背景色が薄くなってしまいました。 これは、GammaCorrectionShaderがデコーダーで、背景色にはガンマ補正のエンコーディングがされていない(線形空間にある)ためです。

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

.glsl
#include <encodings_fragment>

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オブジェクトには、色空間をコンバートするメソッドがあります。今回の場合、エンコードをしたいのでconvertGammaToLinear()またはconvertSRGBToLinear()を使用します。

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の中身を見ると、LinearTosRGBとなっているので、convertSRGBToLinear()を使うと元の色と同じになります。(厳密には微小な誤差が発生しますが...)

GammaCorrectionShader.js
gl_FragColor = LinearTosRGB( tex );

スクリーンショット 2022-01-15 184230 - コピー.png

Textureの補正

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

下図は、左がリソース画像、右がBackgroundに設定したTextureです。
スクリーンショット 2022-01-15 205300.png
TextureにsRGBEncodingを割り当てることで、リソース画像の色合いと同じになります。

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])

もちろん背景にTextureを取った場合でも、Post-processingを使う場合は、GammaCorrectionShaderを追加する必要があります。
スクリーンショット 2022-01-15 211237.png

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です。

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

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

Sandbox

using built-in material

using shader material

まとめ

結構手順が多くて、わりと面倒くさいです。

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

というか、バニラのThree.jsだとWebGLRenderer.outputEncoding = THREE.LinearEncodingがデフォルトになってるんですね。:rolling_eyes:

参照

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
3
Help us understand the problem. What are the problem?