3D / WebGL

src/lib/webgl/ に WebGL のヘルパーと Three.js の薄いラッパが入っている。 書き出し時の context lost からの復旧と、GPU 完了待ちが組み込まれているのが本質的な価値。

レンダラとの相性

書き出しは headless Chromium でフレームをスクラブするが、WebGL の描画は GPU 処理が 非同期なので、ナイーブに撮ると未完了のフレームが写る。FrameScript の WebGL ヘルパーは ここを面倒見てくれる。

素の WebGL を使う

import { useRef } from "react"
import { useWebGLContext, useWebGLFrameWaiter } from "../src/lib/webgl"

const Canvas = () => {
  const canvasRef = useRef<HTMLCanvasElement | null>(null)

  const { glRef } = useWebGLContext(canvasRef, ({ gl }) => {
    // シェーダ・バッファ初期化
    return () => {
      // リソース解放
    }
  })

  useWebGLFrameWaiter(glRef)

  return <canvas ref={canvasRef} />
}

Three.js を使う — <ThreeCanvas />

Three.js のシーン構築・更新を setup コールバックで宣言するラッパ。 setup が返す { scene, camera, update, dispose } がそのままパイプラインに乗る。

import { useAnimation, useVariable } from "../src/lib/animation"
import { BEZIER_SMOOTH } from "../src/lib/animation/functions"
import { seconds } from "../src/lib/frame"
import { ThreeCanvas, THREE, disposeThreeObject } from "../src/lib/webgl/three"

const Scene = () => {
  const progress = useVariable(0)

  useAnimation(async (ctx) => {
    await ctx.move(progress).to(1, seconds(2), BEZIER_SMOOTH)
    await ctx.move(progress).to(0, seconds(2), BEZIER_SMOOTH)
  }, [])

  return (
    <ThreeCanvas
      setup={({ renderer, size }) => {
        renderer.outputColorSpace = THREE.SRGBColorSpace

        const scene = new THREE.Scene()
        const camera = new THREE.PerspectiveCamera(
          45,
          size.cssWidth / size.cssHeight,
          0.1,
          100,
        )
        camera.position.z = 6

        const mesh = new THREE.Mesh(
          new THREE.BoxGeometry(),
          new THREE.MeshStandardMaterial({ color: 0x44aa88 }),
        )
        scene.add(mesh)

        const light = new THREE.DirectionalLight(0xffffff, 1)
        light.position.set(2, 3, 4)
        scene.add(light)

        return {
          scene,
          camera,
          update: ({ frame }) => {
            const t = progress.get(frame)
            mesh.position.x = (t - 0.5) * 3
            mesh.rotation.y = t * Math.PI * 2
          },
          dispose: () => disposeThreeObject(mesh),
        }
      }}
    />
  )
}

ポイント

CSS と Three.js の使い分け

動画全体の演出は CSS + DOM の方が圧倒的に書きやすい。Three.js は3D が要る部分だけ 局所的に <ThreeCanvas> を貼るのが正解。
全画面 3D の動画を作るより、UI 動画の中に 3D の要素を 1 つ差すユースケースが多いはず。