【Three.js】ポストプロセッシングに挑戦

2021年3月3日JavaScript,Three.js

昔のテレビみたい

ポストプロセッシングとは

画面に何かを描画する時に、なんらかのエフェクトやフィルターを適用すること。です。

今回はスライドショーを作って、画像にテレビ画面のような走査線とノイズをつけてみたいと思います。

できた

デモサイトはこちら

ちょっと絵が壊滅的にアレですが…。

テレビがチラチラしてますが静止画だとちょっと分かりにくいですかね。右上のつまみ?をクリックすると画像が切り替わります。

とりあえず全コードをどうぞ。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>テレビみたいな</title>
  <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  <script type="text/javascript" src="../libs/three.min.js"></script>
  <script type="text/javascript" src="../libs/renderers/CSS3DRenderer.js"></script>
  <!-- <script type="text/javascript" src="../libs/controls/TrackballControls.js"></script> -->
  
  <script type="text/javascript" src="../libs/postprocessing/EffectComposer.js"></script>
  <script type="text/javascript" src="../libs/postprocessing/MaskPass.js"></script>
  <script type="text/javascript" src="../libs/postprocessing/ShaderPass.js"></script>
  <script type="text/javascript" src="../libs/shaders/CopyShader.js"></script>
  <script type="text/javascript" src="../libs/postprocessing/RenderPass.js"></script>
  
  <script type="text/javascript" src="../libs/postprocessing/FilmPass.js"></script>
  <script type="text/javascript" src="../libs/shaders/FilmShader.js"></script>
  <script type="text/javascript" src="../libs/postprocessing/GlitchPass.js"></script>
  <script type="text/javascript" src="../libs/shaders/DigitalGlitch.js"></script>
  <style>
      body {
          margin: 0;
          overflow: hidden;
      }
      h2 {
        margin: 0;
        text-align: center;
        padding: 30px;
        background-color: #0097a7;
        color: cornsilk;
      }
  </style>
</head>
<body>

  <div id='header'>
    <h2>とってもおしゃれな<br>スライドショー</h2>
  </div>

  <div id="screen"></div>

  <script type="text/javascript">

    window.onload = function() {
      
      /***** ヘッダーの高さを取得 *****/
      var headerHeight = document.getElementById("header").clientHeight;

      // シーン
      var scene = new THREE.Scene();

      // カメラ
      // アスペクト比
      var aspRatio = window.innerWidth / (window.innerHeight - headerHeight);

      // 視野角
      var fov;
      if (aspRatio > 1) {
        fov = 35;
      } else if (aspRatio > 0.9) {
        fov = 45
      } else if (aspRatio > 0.8) {
        fov = 50;
      } else if (aspRatio > 0.6) {
        fov = 60;
      } else if (aspRatio > 0.5) {
        fov = 70;
      } else {
        fov = 80;
      }
      var camera = new THREE.PerspectiveCamera(fov, aspRatio, 0.1, 1000);
      camera.position.z = 15;
      camera.lookAt(new THREE.Vector3(0, 0, 0));
      
      // レンダラー
      var webGLRenderer = new THREE.WebGLRenderer();
      webGLRenderer.setClearColor(new THREE.Color(0xFFF8DC));
      webGLRenderer.setSize(window.innerWidth, window.innerHeight - headerHeight);
      webGLRenderer.shadowMap.enabled = true;
      document.getElementById("screen").appendChild(webGLRenderer.domElement);

      // DOM要素用のレンダラー
      var renderer = new THREE.CSS3DRenderer();
      renderer.setSize(window.innerWidth, window.innerHeight - headerHeight);
      renderer.domElement.style.position = "absolute";
      renderer.domElement.style.top = headerHeight + "px";
      renderer.domElement.style.left = "0px";
      document.getElementById("screen").appendChild(renderer.domElement);

      // テレビ
      var tvElem = document.createElement("img");
      tvElem.setAttribute("src","../assets/textures/tv.png");
      tvElem.style.width = "5px";
      tvElem.style.height = "5px";
      var tvObj = new THREE.CSS3DObject(tvElem);
      tvObj.position.z = 5;
      scene.add(tvObj);

      // つまみ
      var dialElem =document.createElement("img");
      dialElem.setAttribute("src","../assets/textures/dial.png");
      dialElem.style.with = "1px";
      dialElem.style.height = "1px";
      dialElem.style.cursor = "pointer";
      var dialObj = new THREE.CSS3DObject(dialElem);
      dialObj.position.x = 1.6;
      dialObj.position.y = 0.5;
      dialObj.position.z = 5;
      scene.add(dialObj);

      // テクスチャ読み込み
      var textureLoader = new THREE.TextureLoader();
      var texture = textureLoader.load("../assets/textures/animals/cat.jpg");

      // パネル
      var geo = new THREE.PlaneGeometry(5, 3.75, 1, 1);
      var mat = new THREE.MeshBasicMaterial();
      mat.map = texture;
      var panel = new THREE.Mesh(geo, mat);
      scene.add(panel);

      /***** ポストプロセッシングの設定 ここから *****/
      // レンダ―パス
      var renderPass = new THREE.RenderPass(scene, camera);

      // マスク
      var televisionMask = new THREE.MaskPass(scene, camera);

      // テレビのようなエフェクト
      var effectFilm = new THREE.FilmPass(1.0, 0.5, 100, false);

      // 電子的なノイズのようなエフェクト
      var effectGlitch = new THREE.GlitchPass(64);
      effectGlitch.goWild = true;
      effectGlitch.enabled = false;

      // マスククリア
      var clearMask = new THREE.ClearMaskPass();

      // コピーシェーダー
      var effectCopy = new THREE.ShaderPass(THREE.CopyShader);
      effectCopy.renderToScreen = true;

      // コンポーザー
      var composer = new THREE.EffectComposer(webGLRenderer);
      composer.renderTarget1.stencilBuffer = true;
      composer.renderTarget2.stencilBuffer = true;
      composer.addPass(renderPass);
      composer.addPass(televisionMask);
      composer.addPass(effectFilm);
      composer.addPass(effectGlitch);
      composer.addPass(clearMask);
      composer.addPass(effectCopy);
      /***** ポストプロセッシングの設定 ここまで *****/

      // // カメラのコントロール
      // var trackballControls = new THREE.TrackballControls(camera);
      // trackballControls.panSpeed = 0.2;
      // trackballControls.rotateSpeed = 3.0;
      // trackballControls.minDistance = 10;
      // trackballControls.maxDistance = 1000;

      // 描画関数
      var clock = new THREE.Clock();
      function render() {
        webGLRenderer.autoClear = false;
        var delta = clock.getDelta();
        // trackballControls.update(delta);
        requestAnimationFrame(render);
        composer.render(delta);
        renderer.render(scene, camera);
      }

      // ノイズを消す
      function deleteNoise() {
        effectGlitch.enabled = false;
        // イベントリスナー追加
        dialElem.addEventListener("click", onDialClick);
      }

      /********************************/
      /***** イベントリスナー設定 *****/
      /********************************/

      /***** ウィンドウサイズ変更 *****/
      window.addEventListener("resize", onResize);
      function onResize() {
        // アスペクト比
      var aspRatio = window.innerWidth / (window.innerHeight - headerHeight);

        // 視野角
        var fov;
        if (aspRatio > 1) {
          fov = 35;
        } else if (aspRatio > 0.9) {
          fov = 45
        } else if (aspRatio > 0.8) {
          fov = 50;
        } else if (aspRatio > 0.6) {
          fov = 60;
        } else if (aspRatio > 0.5) {
          fov = 70;
        } else {
          fov = 80;
        }
        camera.aspect = aspRatio;
        camera.fov = fov;
        camera.updateProjectionMatrix();
        webGLRenderer.setSize(window.innerWidth, window.innerHeight - headerHeight);
        renderer.setSize(window.innerWidth, window.innerHeight - headerHeight);
        render();
      }

      /***** つまみをクリック *****/
      // 画像のパス
      var imgPath = [
        "../assets/textures/animals/cat.jpg",
        "../assets/textures/animals/cow.png",
        "../assets/textures/animals/dog.jpg",
        "../assets/textures/animals/kiyoko256.png"
      ]

      var cnt = 0;
      dialElem.addEventListener("click", onDialClick);
      function onDialClick() {
        
        // イベントリスナー削除(連打されてもいいように)
        dialElem.removeEventListener("click", onDialClick);
        
        // つまみを回転させる
        dialObj.rotation.z += Math.PI / 180 * -(360 / imgPath.length);
        
        // 画像を切り替える
        cnt += 1;
        if (cnt > imgPath.length-1) {
          cnt = 0;
        }
        mat.map = textureLoader.load(imgPath[cnt]);
        
        // ノイズを発生させる
        effectGlitch.enabled = true;
        
        // ノイズを消す
        setTimeout(deleteNoise, 300);
      }

      // 描画
      render();
    }
  </script>
</body>
</html>

いろいろ大変でした。ポストプロセッシングのところ以外にも説明したいことがあるんですけど、ゴチャゴチャしてしまうのでそれはまた次回にします。

コンポーザーが大事

そうなんです。ポストプロセッシングで大事な役割を担っているのがコンポーザーと呼ばれるものになります。

// コンポーザー
var composer = new THREE.EffectComposer(webGLRenderer);
composer.renderTarget1.stencilBuffer = true;
composer.renderTarget2.stencilBuffer = true;
composer.addPass(renderPass);
composer.addPass(televisionMask);
composer.addPass(effectFilm);
composer.addPass(effectGlitch);
composer.addPass(clearMask);
composer.addPass(effectCopy);

コンポーザーは"THREE.EffectComposer"のコンストラクタにレンダラーを渡してあげることで取得できます。

で、その取得したコンポーザーに対して"addPass"関数で様々なパスを追加していきます。パスというのは、画面に施すエフェクトのことだと思っておけば大丈夫だと思います。今回の場合だったら、「テレビみたいな走査線」とか「チャンネル変えた時のノイズ」のことですね。

追加するパスの順番にも気をつけないといけません。いちばん始めは必ず"THREE.RenderPass"を追加します。コイツはカメラで撮影した映像を内部的に描画します。で、その映像に対してあとから追加されたパスがそれぞれのエフェクトを付けていくようです。

そうしてできあがった映像をコンポーザーの"render"関数を使ってディスプレイに映し出します。

// 描画関数
function render() {
  (略)
  composer.render(delta);
  (略)
}

ここまでの流れを絵にするとこんな感じ↓。

画面全体にエフェクトがかかっちゃう問題

今回はテレビ画面にだけエフェクトをかけたいので、これだと困っちゃう。これはマスクをかけてあげることで解決します。コンポーザーを設定しているコードをもう一度見てみましょう。

// コンポーザー
var composer = new THREE.EffectComposer(webGLRenderer);
composer.renderTarget1.stencilBuffer = true;
composer.renderTarget2.stencilBuffer = true;
composer.addPass(renderPass);
composer.addPass(televisionMask);
composer.addPass(effectFilm);
composer.addPass(effectGlitch);
composer.addPass(clearMask);
composer.addPass(effectCopy);

“televisionMask"をコンポーザーに追加していますね。これがマスクです。このマスクを追加してから、マスクをクリアする"composer.addPass(clearMask)"までに追加されたパスにはマスクがかかります。

“televisionMask"は"THREE.MaskPass"というクラスのオブジェクトです。で、オブジェクトを作るコードがこれ↓。

// マスク
var televisionMask = new THREE.MaskPass(scene, camera);

コンストラクタに"scene"を渡しています。こうしてあげると、この"scene"の中に入れたモノにだけエフェクトを適用することができます。

今回"scene"に入っているのはテレビの中の画像だけなので、そこ以外の場所にはエフェクトがかからなくなります。

renderToScreenプロパティについて

コンポーザーの最後に追加している"effectCopy"。こんな感じで設定しています。

// コピーシェーダー
var effectCopy = new THREE.ShaderPass(THREE.CopyShader);
effectCopy.renderToScreen = true;

コンポーザーの最後に追加したパスは"renderToScreen"をtrueにする必要があります。そうしないと、そこまでであれこれとエフェクトかけた映像が画面に映りません。なぜかは分かりませんが、そういうもんだと割り切りましょう。

で、今回かけたいエフェクトは"effectFilm"と"effectGlitch"のふたつです。そうなると、この"effectCopy"は何なのでしょうか?"effectGlitch"が最後のパスなんだから"effectGlitch.renderToScreen = true"ってするんじゃないの?って。

実は、パスには「自分で描画できるパス」と「自分で描画できないパス」があるみたいなんです。"effectGlitch"は「自分で描画できないパス」なので、代わりに"effectCopy"に描画してもらう必要があります。なので"effectCopy.renderToScreen = true"となります。

あとがき

むずかしい。コード書き換え⇀実行⇀変化を見る⇀コード書き換え⇀実行…これをずっと繰り返して、やっと「あ~なんとなく…」って感じです。

それを文字で説明するっていうのもなかなか…。分かりにくい説明になってしまってスイマセン。まだ説明したいところがありますが、いったんお終いにします。ありがとうございました。

おしゃれ度

★★★☆☆

Posted by ナカタ