r3f Aquarium example の解説

drcmda による r3f の example「Aquarium」の解説。
概観
useFrame()内で、cubeGeometryをレンダリング先として設定し、シーンをテクスチャとしてレンダリングuseMask()で、オリジナルの亀が見えないようにマスク- 色収差や屈折はすべてフラグメントシェーダーで演算
オフスクリーンレンダリング
単純にシーンをオフスクリーンレンダリングして、cubeGeometryをレンダリングターゲットに設定して、テクスチャに描画している。
useFrame()でループを回して、高速にレンダリングターゲット先をキャンバス全体とcubeGeometryで切り替えているだけ。屈折や滲みなどの処理はfragment shaderで物理演算している。
オリジナルの亀のレンダリングについて
サンプルのコードだと、useMask()で亀が見えないようにマスクしているので、オリジナルの亀と、キューブのテクスチャとしての亀と、2回レンダリングされている。
useMask()はmeshのrenderOrderをマイナス値に設定しているだけなので、見えないがレンダリングされている。
別の解法としては、亀のvisibleプロパティを一瞬だけtrueにして、FBOを使用してバッファに保存し、テクスチャとしてレンダリングした後に、visibleをまたfalseに戻せば良い。.visible = falseならレンダリングされないので。
function Aquarium() {
const turtleRef = useRef()
const buffer = useFBO()
useFrame((state) => {
turtleRef.current.visible = true // 一瞬trueにする
state.gl.setRenderTarget(buffer)
state.gl.render(state.scene, state.camera)
state.gl.setRenderTarget(null)
turtleRef.current.visible = false // 隠す
})
return (
<>
<Turtle ref={turtleRef} />
<boxGeometry>
<MeshTransmissionMaterial />
</boxGeometry>
</>
)
}
屈折の再現について
キューブの面ごとに異なるテクスチャを渡しているというよりも、フラグメントシェーダーで各面の法線ベクトルを使用して計算している。
実際のコードでは、anisotropy、aberrationなどの様々なプロパティを考慮して実装されているが、画像がずれて見える = 光が屈折している のは refraction 周りで処理されている。
ここで出射ベクトルの計算を行っている。
// ior (index of refraction) = 屈折率
vec3 refractionVector = refract( - v, normalize( n ), 1.0 / ior );
refractの物理演算
入射ベクトルと法線ベクトルの内積を使って、出射ベクトルを算出している。
逆に、極端に分割数を減らした球体などでは、各面の法線ベクトルではなく、元の球体の法線ベクトルとして計算されるように見える。下記の場合、辺の境界線が見えないので。
<sphereGeometry args={[1, 4, 4]} />

終わりに
キューブの奥の面に反射した亀が見えるとより実物に近づくはず。