feature image

2023年4月5日 | ブログ記事

🧬スリットスキャンをReactとThree.jsで🧬

この記事は、2023年traP新歓ブログリレー28日目の記事です。

こんにちは。20Bの@Rasです。東工大に合格された方はおめでとうございます。今回のブログリレーで会うのは2回目ですね。

少し前にスリットスキャン (Slit-scan)という技術に興味を持ちました。スリットスキャン自体はかなり前からあるらしいんですが、興味を持つきっかけになったのはこの動画です。

今回はReactとThree.jsを使って簡単なスリットスキャンを実装してみました。

ちなみにAdobe After Effectsみたいなソフトを使うと簡単に実現できるらしいです。

参考記事

作ったもの

slitscan3d
Slit-Scan with Three.js
GitHub - ras0q/slitscan3d: a 3D Slit-Scan effect using Three.js.
a 3D Slit-Scan effect using Three.js. Contribute to ras0q/slitscan3d development by creating an account on GitHub.

結構重いのでスマホとかだと見れないかもです。

箱に動画が映し出され、パラメータを調整することで断面を操作することができます。
カメラでのストリーミングにも対応しているので自分の顔をぐにゃぐにゃしてみてください。

(このサイトではカメラ映像がサーバーに送られることはないので安心してね)。

スリットスキャンってなに

こういうやつです。

もっと滑らかなやつだとこういうやつ。

背景は動いていないのに人とドアだけが動いてるのとか興味深いですよね。

しくみ

まず、動画の各フレームを一列に並べたものを3次元空間に投影します。

次に、並べたフレームを時間軸からずれるように斜めにカットします。

カットしてできた断面を正面から見ると、1枚の画像なのに複数の時間軸を持った不思議な画像になります。
(背景などの静止した物体は時間軸が変わっても動かないため、動いている物体のみが波打って見えます)

そしてフレームの列を時間経過で動かすことで、節の最初に紹介した動画のような不思議な動画を作ることができます。

もちろん断面は四角形である必要はなく、自由に断面を作ることでさらに応用的なフレームを作ることができます。

slitscan3dでは、断面を動かすことで縦だけでなく横や斜めに分割されたスリットスキャンを実現できるようにしました。

実装

再度レポジトリを貼ります。

GitHub - ras0q/slitscan3d: a 3D Slit-Scan effect using Three.js.
a 3D Slit-Scan effect using Three.js. Contribute to ras0q/slitscan3d development by creating an account on GitHub.

タイトルにもあるように、今回はReactとThree.jsを使ってslitscan3dを作りました。
Reactはほぼ未経験、Three.jsについては全くの未経験だったのですが、文法や仕様を理解する良い経験になりました。

R3Fはいいぞ

生のThree.jsをReact上で触るのは結構面倒くさい(らしい)のですが、Three.jsをReactのコンポーネント風に書けるReact Three Fiber(R3F)を使ったことでかなり楽に書くことができました。

React Three Fiber Documentation
React-three-fiber is a React renderer for three.js

R3F公式ドキュメントに載っている簡単な例を見ても、R3Fを使うことでシンプルに記述できることが分かると思います。

const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)

const renderer = new THREE.WebGLRenderer()
renderer.setSize(width, height)
document.querySelector('#canvas-container').appendChild(renderer.domElement)

const mesh = new THREE.Mesh()
mesh.geometry = new THREE.BoxGeometry()
mesh.material = new THREE.MeshStandardMaterial()

scene.add(mesh)

function animate() {
  requestAnimationFrame(animate)
  renderer.render(scene, camera)
}

animate()
<Canvas>
  <mesh>
    <boxGeometry />
    <meshStandardMaterial />
  </mesh>
</Canvas>

実装の概要

slitscan3dにおけるスリットスキャンの描画処理は主に src/components/SlitScan/SlitScanGroup.tsx に書かれています。

与えられた動画を <canvas> に描画し、一定時間ごとのキャプチャをallTexturesに保存します。

  const videoCanvas = useMemo(() => {
    const canvas = document.createElement('canvas')
    canvas.width = video.videoWidth
    canvas.height = video.videoHeight
    return canvas
  }, [video])
  const videoCtx = videoCanvas.getContext('2d', { willReadFrequently: true })

  if (videoIsValid()) {
    videoCtx?.drawImage(video, 0, 0, video.videoWidth, video.videoHeight)
  }
  const createFrameLoop = useCallback(() => {
    if (videoIsValid()) {
      allTextures.push(new CanvasTexture(videoCanvas))
      setAllTextures(allTextures)
    }
  }, [video])
  useAnimationFrame(createFrameLoop)

ある程度フレームが揃ってきたらフレームの列を回転させます。

  // callbackを毎フレーム呼ぶR3Fのhook
  useFrame(() => {
	// ...
    frameIndex.current = (frameIndex.current + 1) % allTextures.length
    setTextures(textures.map((_, i) => allTextures[(frameIndex.current + i) % allTextures.length]))
  })

最後に各フレームの描画位置を少しずつずらしながら<boxGeometry>に描画することでアニメーションを作成することができます。

また、clippingPlanesを指定してオブジェクトをクリッピングすることで、スリットスキャンの特徴である断面を再現することができます。

  // clip the front of the extra drawn box
  const additionalClipPlane = useMemo(() => new Plane(new Vector3(0, 0, -1), Math.ceil(depth / 2)), [depth])

  return (
    <group>
      {textures.map((texture, i) => (
        <mesh key={texture.id} castShadow position={[0, 0, i * (depth / textures.length)]}>
          <boxGeometry args={[width, height, depth]} />
          <meshStandardMaterial
            map={texture}
            clippingPlanes={[additionalClipPlane, ...clipPlanes]}
            clipShadows={true}
            side={DoubleSide}
          />
        </mesh>
      ))}
    </group>
  )

おわりに

まだReact hooksが正しく使えてなかったりslitscan3dのサイト自体がかなり重かったりと改善点はありますが、これまでやったことがないことをやるのは楽しかったです。
traPには基本的にどの分野にも先駆者がいるので気軽に質問できる環境が整っています。気になった方は是非部室まで!

明日は@ikura-hamu、@jippoの記事です。お楽しみに!

Ras icon
この記事を書いた人
Ras

20B。アライグマです。

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2023年4月17日
ポケモンを飼いたい夢を叶える
tqk icon tqk
2023年4月25日
【驚愕】作曲4年目だった男が大学3年間ゲームサウンドに関わった末路...【ゲームサウンドのお仕事について】
tenya icon tenya
2023年3月20日
traPグラフィック班の活動紹介(Ver.2023)
NABE icon NABE 他
2023年4月27日
Vulkanのデバイスドライバを自作してみた
kegra icon kegra
2023年4月25日
15時間でゲームを作った #Oxygenator
Komichi icon Komichi
2022年9月26日
競プロしかシラン人間が web アプリ QK Judge を作った話
tqk icon tqk
記事一覧 タグ一覧 Google アナリティクスについて