本文へスキップ

r3f Aquarium example の解説

2025.03
オフスクリーンレンダリングと屈折。MeshTransmissionMaterial が何をしているかを追う

r3f の Aquarium example

drcmda による r3f の example「Aquarium」の解説。

概観

  1. useFrame()内で、cubeGeometryをレンダリング先として設定し、シーンをテクスチャとしてレンダリング
  2. useMask()で、オリジナルの亀が見えないようにマスク
  3. 色収差や屈折はすべてフラグメントシェーダーで演算

オフスクリーンレンダリング

単純にシーンをオフスクリーンレンダリングして、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の物理演算

入射ベクトルと法線ベクトルの内積を使って、出射ベクトルを算出している。

(η:IOR−1, n:法線ベクトル, v:入射ベクトル, r:出射ベクトル)k=1.0−η2×(1.0−dot(n,v)2)where r={vec3(0.0)if k<0.0η×v−(η×dot(n,v)+k12)×notherwise(\eta: IOR^{-1},\ \mathbf{n}: 法線ベクトル,\ \mathbf{v}: 入射ベクトル,\ \mathbf{r}: 出射ベクトル)\\ \begin{align*} k &= 1.0 − η^2 \times (1.0 − dot(\mathbf{n},\mathbf{v})^2)\\ \text{where}\ \mathbf{r} &= \begin{cases} vec3(0.0) &\text{if}\ k < 0.0\\ η \times \mathbf{v} - (η \times dot(\mathbf{n}, \mathbf{v}) + {k}^{\frac{1}{2}}) \times \mathbf{n} &\text{otherwise} \end{cases} \end{align*}

逆に、極端に分割数を減らした球体などでは、各面の法線ベクトルではなく、元の球体の法線ベクトルとして計算されるように見える。下記の場合、辺の境界線が見えないので。

<sphereGeometry args={[1, 4, 4]} />

分割数を落とした球体

終わりに

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