Scratch 3.0 で Theta写真をぐるぐる回そう！

## オリジナルブロックを作ろう！

みんな大好き[Scratch](https://scratch.mit.edu/)。3.0では自分で拡張機能が作れると聞いていたが、実際にどうやって作るのか知らなかった。少し調べてみたところ、[Scratch 3.0でオリジナルブロックをつくろう](https://qiita.com/Hiroyuki_OSAKI/items/a46e1c881d3aed4661f7)にバッチリ書いてあった。なんだ、Reactで作れるじゃないかということで、ゴールデンウィークのお楽しみとして、[Theta](https://theta360.com/ja/)写真のビューアー作りに挑戦しました。

## ソースコードはどこにある？

Scratch 3.0の拡張機能は、現在までのところ、ソースコードを自分で編集して追加する必要があります。[ここ](https://github.com/LLK)にあるレポジトリが実体ですが、scratch-vm と scratch-gui から調査を始めました。

Theta写真(360度写真)をブラウザで表示するためには様々な方法があると思います。ScratchがReactで書かれているならば、[React 360](https://facebook.github.io/react-360/) や [aframe-react](https://github.com/supermedium/aframe-react) を使えば瞬殺なんじゃないかと、当初は考えていました。

しばらく(実際はかなり長いこと)ソースコードを眺めていても、一向に取りつく島が見つからず半ば諦めようかと思ったところで、scratch-renderを使えばなんとかなるかもと思い至りました。[Scratch 3.0 を Hackしよう。 scratch-render.js で 何か作ってみよう](https://qiita.com/kyorohiro/items/71b5a0c366b1e62600f7)にあるように、このパッケージがScratchのレンダリング処理の核心だと分かります。

## Three.jsのレンダリング結果を転送する

scratch-renderそのものに手を入れるのは、私には難しそうだったので、scratch-renderのインターフェイスを使ってTheta写真を描画しました。基本的なアイデアは、[Three.js equirectangular panorama demo](https://threejs.org/examples/webgl_panorama_equirectangular.html)のコードでレンダリングしたデータを、ビットマップとしてscratch-renderに転送しています。

```javascript
    init() {
        // https://threejs.org/examples/webgl_panorama_equirectangular.html
        this.lon = 0;
        this.lat = 0;
        this.phi = 0;
        this.theta = 0;

        this.camera = new THREE.PerspectiveCamera(75, 480 / 360, 1, 2000);
        this.camera.target = new THREE.Vector3(0, 0, 0);

        this.scene = new THREE.Scene();
        var geometry = new THREE.SphereBufferGeometry(500, 60, 40);
        // invert the geometry on the x-axis so that all of the faces point inward
        geometry.scale(-1, 1, 1);
        var texture = new THREE.TextureLoader().load(img);
        var material = new THREE.MeshBasicMaterial({ map: texture });
        this.mesh = new THREE.Mesh(geometry, material);
        this.scene.add(this.mesh);

        this.renderer = new THREE.WebGLRenderer();
        this.renderer.setSize(480, 360);
        this.renderer.render(this.scene, this.camera);

        this.canvas = document.createElement("canvas");
        this.canvas.width = 480;
        this.canvas.height = 360;

        this.loadImage(this.renderer.domElement.toDataURL())
            .then(res => {
                this.ctx = this.canvas.getContext("2d");
                this.ctx.drawImage(res, 0, 0);
                this.skinId = this.runtime.renderer.createBitmapSkin(
                    this.canvas,
                    1
                );
                const drawableId = this.runtime.renderer.createDrawable(
                    this.layerGroup.testPattern
                );
                this.runtime.renderer.updateDrawableProperties(drawableId, {
                    skinId: this.skinId
                });
            })
            .catch(e => console.error(e));
    }
```

アニメーション処理も同様で、カメラを回転させた状態で描画したデータを、再びscratch-renderに転送しています。

```javascript
    animate() {
        requestAnimationFrame(this.animate.bind(this));
        this.lon += this.interval;
        this.lat = Math.max(-85, Math.min(85, this.lat));
        this.phi = THREE.Math.degToRad(90 - this.lat);
        this.theta = THREE.Math.degToRad(this.lon);

        this.camera.target.x =
            500 * Math.sin(this.phi) * Math.cos(this.theta);
        this.camera.target.y = 500 * Math.cos(this.phi);
        this.camera.target.z =
            500 * Math.sin(this.phi) * Math.sin(this.theta);

        this.camera.lookAt(this.camera.target);
        this.renderer.render(this.scene, this.camera);

        this.loadImage(this.renderer.domElement.toDataURL())
            .then(res => {
                this.ctx.drawImage(res, 0, 0);
                this.runtime.renderer.updateBitmapSkin(
                    this.skinId,
                    this.canvas,
                    1
                );
            })
            .catch(e => console.error(e));
    }
```

こんな感じで動きました！

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/Scratch?src=hash&amp;ref_src=twsrc%5Etfw">#Scratch</a> で <a href="https://twitter.com/hashtag/Theta?src=hash&amp;ref_src=twsrc%5Etfw">#Theta</a> 写真をぐるぐる　これがやりたかったんだよ <a href="https://t.co/q1F0vwCOO3">pic.twitter.com/q1F0vwCOO3</a></p>&mdash; Masahiro Kisono (@mkisono) <a href="https://twitter.com/mkisono/status/1123759014272282625?ref_src=twsrc%5Etfw">May 2, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

[こちら](https://nifty-wing-efec91.netlify.com/)で動作を試せます。右下の「拡張機能を追加」から"Scratch 360"を選んでください。

ソースコードは[ここ](https://github.com/mkisono/scratch-vm/tree/360)にあります。

## 動いたものの・・

Scratchのレンダリングは、WebGLで行われていることが分かりました。本来だったら、3Dのレンダリングだってヌルヌルに動くはずですが、私の作った拡張ではそうなっていません。scratch-render は twgl で作られていて、これの使い方が分からない。仕方ないので、Three.jsでレンダリングしてビットマップを転送していますが、コレジャナイ感がすごいです。scratch-renderに手を入れれば、はるかに滑らかに描画ができると思います。

とはいえ、3Dグラフィックスが無くても Scratchは素晴らしいシステムなので、次は別のものを作ってみようかなって思いました。

## 参考

Ishiharaさんのコンテンツを参考にさせてもらいました。

* [大人のためのScratch](https://otona-scratch.champierre.com/)