🎥
Three.js - Post Processing
基本
仕組み
- canvas ではなく render target と呼ばれるものに対してテクスチャを描画する
- render target は Three.js 特有の用語で、多くの場合 buffer と呼ばれる
- このテクスチャはカメラに相対する平面に適用され、画面全体を覆う
- この平面に適当なシェーダーをもつマテリアルが適用されることで、 post-processing の効果(Three.js では passes と呼ぶ)が与えられる
- 複数の passes を適用するとき、ひとつの render target のテクスチャへの読み書きは同時にはできないので、2つの render target を交互に使って描画する
- これを ping pong buffering という
- 最後の pass を適用したテクスチャは render target ではなく canvas に描画され、画面に表示される
EffectComposer
- https://threejs.org/docs/#examples/en/postprocessing/EffectComposer
- やることは基本
EffectComposer
にaddPass
するだけ -
EffectComposer
のインスタンスに対してもsetSize
、setPixelRatio
が必要 -
effectComposer.render()
を毎フレーム呼ぶ
import {
DotScreenPass,
EffectComposer,
GLTFLoader,
OrbitControls,
RenderPass,
} from "three/examples/jsm/Addons.js"
// ...
// Renderer
const renderer = new THREE.WebGLRenderer({ canvas })
renderer.setSize(width, height) renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
// Post processing
const effectComposer = new EffectComposer(renderer)
effectComposer.setSize(width, height) effectComposer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
const renderPass = new RenderPass(scene, camera)
const dotScreenPass = new DotScreenPass()
effectComposer.addPass(renderPass)
effectComposer.addPass(dotScreenPass)
const tick = () => {
effectComposer.render() // renderer ではなく effectComposer
requestAnimationFrame(tick)
}
tick()
Passes
RenderPass
- renderer の出力を最初の入力とする
- 多くの場合この pass から始まる
const renderPass = new RenderPass(scene, camera)
DotScreenPass
- 白黒のラスタライズ
const dotScreenPass = new DotScreenPass()
GlitchPass
- 画面が荒れる
-
goWild
でずっと荒れる
const glitchPass = new GlitchPass()
glitchPass.goWild = true
ShaderPass
-
ShaderPass
にシェーダーを渡すことで実現する pass もある
RGBShiftPass
const rgbShiftPass = new ShaderPass(RGBShiftShader)
GammaCorrectionPass
- カラースペースを SRGB にする
- レンダラーのカラースペースは render target には適用されない
- ガンマ色補正について
-
最後の方に適用すること!
- でもアンチエイリアスの pass よりは前
const gammaCorrectionPass = new ShaderPass(GammaCorrectionShader)
UnrealBloomPass
- グレアやライトセーバーっぽい輝き(Light glow)を与える
- パラメータ
- strength: 強さ
- radius: 輝きがどのくらい遠くまで拡散するか
- threshold: どのくらいの明るさから glow し始めるか
const unrealBloomPass = new UnrealBloomPass()
unrealBloomPass.strength = 0.3
unrealBloomPass.radius = 1
unrealBloomPass.threshold = 0.6
カスタム pass を作る
- カスタム pass = カスタムシェーダーを ShaderPass にわたしたもの
TintPass
- 色調補正シェーダー
-
uniform
としてその時点での render target がもつテクスチャがtDiffuse
に入る- 値は自動的に入るので、最初は
null
でいい
- 値は自動的に入るので、最初は
-
uTint
は RGB 各色どれだけ補正するかの値をもつベクトルだが、再利用性のためにシェーダーの内部では値を設定しない -
vUv
がvarying
として与えられる - ピクセルの
rgba
はvec4 color = texture2D(tDiffuse, vUv);
で取り出せる
const TintShader = {
uniforms: {
tDiffuse: { value: null },
uTint: { value: null },
},
vertexShader: `
varying vec2 vUv;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
vUv = uv;
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform vec3 uTint;
varying vec2 vUv;
void main() {
vec4 color = texture2D(tDiffuse, vUv);
color.rgb += uTint;
gl_FragColor = color;
}
`,
}
const tintPass = new ShaderPass(TintShader)
tintPass.material.uniforms.uTint.value = new THREE.Vector3()
effectComposer.addPass(tintPass)
gui.add(tintPass.material.uniforms.uTint.value, 'x').min(- 1).max(1).step(0.001).name('red')
gui.add(tintPass.material.uniforms.uTint.value, 'y').min(- 1).max(1).step(0.001).name('green')
gui.add(tintPass.material.uniforms.uTint.value, 'z').min(- 1).max(1).step(0.001).name('blue')
DisplacementPass
- サインカーブで波打つようなエフェクト
- UV を移動させて、移動先のピクセルを取ってくるイメージ
const DisplacementShader = {
uniforms: {
tDiffuse: { value: null },
},
vertexShader: `
varying vec2 vUv;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
vUv = uv;
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float uTime;
varying vec2 vUv;
void main() {
vec2 newUv = vec2(
vUv.x,
vUv.y + sin(vUv.x * 10.0) * 0.1
);
vec4 color = texture2D(tDiffuse, newUv);
gl_FragColor = color;
}
`,
}
const displacementPass = new ShaderPass(DisplacementShader)
effectComposer.addPass(displacementPass)
InterfacePass
- ガラス越し?に見ているかのようなエフェクト
-
uNormalMap
テクスチャを貼り付ける - 別の方法があるっぽい
const InterfaceShader = {
uniforms: {
tDiffuse: { value: null },
uNormalMap: { value: null },
},
vertexShader: `
varying vec2 vUv;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
vUv = uv;
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float uTime;
uniform sampler2D uNormalMap;
varying vec2 vUv;
void main() {
vec3 normalColor = texture2D(uNormalMap, vUv).xyz * 2.0 - 1.0;
vec2 newUv = vUv + normalColor.xy * 0.1;
vec4 color = texture2D(tDiffuse, newUv);
vec3 lightDirection = normalize(vec3(- 1.0, 1.0, 0.0));
float lightness = clamp(dot(normalColor, lightDirection), 0.0, 1.0);
color.rgb += lightness * 2.0;
gl_FragColor = color;
}
`,
}
const interfacePass = new ShaderPass(InterfaceShader)
interfacePass.material.uniforms.uNormalMap.value = textureLoader.load("/textures/interfaceNormalMap.png")
effectComposer.addPass(interfacePass)
アンチエイリアス
- EffectComposer はデフォルトのアンチエイリアスをサポートしていないので、他の方法でやる必要がある
WebGLRenderTarget をカスタマイズする方法
- EffectComposer が使用する WebGLRenderTarget を書き換える
- WebGL2 をサポートしていないブラウザでは適用されないが、そんなブラウザはもうほとんどないので気にしなくて良さそう
- ブラウザがサポートしていない場合は単に無視されるだけなので、本当にデメリットなし
const renderTarget = new THREE.WebGLRenderTarget(
undefined, // setSize で設定するのでなんでもいい
undefined, // setSize で設定するのでなんでもいい
{
samples: renderer.getPixelRatio() === 1 ? 2 : 0,
},
)
const effectComposer = new EffectComposer(renderer, renderTarget)
アンチエイリアスの pass を適用する方法
- パフォーマンスが落ち、結果も少し異なる
- Pass いろいろ
- FXAA: パフォーマンス◎ 質△
- SMAA: パフォーマンス◯ 質◯
- SSAA: パフォーマンス△ 質◎
- 他にもある
-
最後の方に適用すること!
- GammaCorrectionPass よりも!
- WebGL2 をサポートしていない場合だけ有効にする
if(renderer.getPixelRatio() === 1 && !renderer.capabilities.isWebGL2) {
const smaaPass = new SMAAPass()
effectComposer.addPass(smaaPass)
}
- ちなみに WebGL1 での挙動は renderer を WebGL1Renderer に差し替えることで確認できる
const renderer = new WebGL1Renderer({
// ...
})
Discussion