オリジナルブロックを作ろう!
みんな大好きScratch。3.0では自分で拡張機能が作れると聞いていたが、実際にどうやって作るのか知らなかった。少し調べてみたところ、Scratch 3.0でオリジナルブロックをつくろうにバッチリ書いてあった。なんだ、Reactで作れるじゃないかということで、ゴールデンウィークのお楽しみとして、Theta写真のビューアー作りに挑戦しました。
ソースコードはどこにある?
Scratch 3.0の拡張機能は、現在までのところ、ソースコードを自分で編集して追加する必要があります。ここにあるレポジトリが実体ですが、scratch-vm と scratch-gui から調査を始めました。
Theta写真(360度写真)をブラウザで表示するためには様々な方法があると思います。ScratchがReactで書かれているならば、React 360 や aframe-react を使えば瞬殺なんじゃないかと、当初は考えていました。
しばらく(実際はかなり長いこと)ソースコードを眺めていても、一向に取りつく島が見つからず半ば諦めようかと思ったところで、scratch-renderを使えばなんとかなるかもと思い至りました。Scratch 3.0 を Hackしよう。 scratch-render.js で 何か作ってみようにあるように、このパッケージがScratchのレンダリング処理の核心だと分かります。
Three.jsのレンダリング結果を転送する
scratch-renderそのものに手を入れるのは、私には難しそうだったので、scratch-renderのインターフェイスを使ってTheta写真を描画しました。基本的なアイデアは、Three.js equirectangular panorama demoのコードでレンダリングしたデータを、ビットマップとしてscratch-renderに転送しています。
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に転送しています。
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));
}
こんな感じで動きました!
#Scratch で #Theta 写真をぐるぐる これがやりたかったんだよ pic.twitter.com/q1F0vwCOO3
— Masahiro Kisono (@mkisono) May 2, 2019
こちらで動作を試せます。右下の「拡張機能を追加」から"Scratch 360"を選んでください。
ソースコードはここにあります。
動いたものの・・
Scratchのレンダリングは、WebGLで行われていることが分かりました。本来だったら、3Dのレンダリングだってヌルヌルに動くはずですが、私の作った拡張ではそうなっていません。scratch-render は twgl で作られていて、これの使い方が分からない。仕方ないので、Three.jsでレンダリングしてビットマップを転送していますが、コレジャナイ感がすごいです。scratch-renderに手を入れれば、はるかに滑らかに描画ができると思います。
とはいえ、3Dグラフィックスが無くても Scratchは素晴らしいシステムなので、次は別のものを作ってみようかなって思いました。
参考
Ishiharaさんのコンテンツを参考にさせてもらいました。