(この記事は「CRESCO Advent Calender 2019」12日めの記事です)

こんにちは、UXデザインセンターのsgi-changです。

今年もAdvent Calendarに参加させて頂くことになりました。

ちょうど一年前「WebGL、three.jsでワクワクしてみた」を書いたのですが、

何を書こうかと色々迷った挙句…私の中で最近ますますホットな3DCGの記事が書きたくて、

もうすぐクリスマス、そしてもうすぐ1年も終わるし、

せっかくAdvent Calendarだし、

今年もやっぱりThree.jsでワクワクしてみたいなと思い、

Three.jsでひとりハッカソン(※)に挑戦してみることにしました。
(しつこくてすみません。)

※ちなみにハッカソンとは…
ハック(hack)とマラソン(marathon)を合わせた混成語。
ソフトウェア開発分野のプログラマやグラフィックデザイナー、ユーザインタフェース設計者、プロジェクトマネージャらが集中的に作業をするソフトウェア関連プロジェクトのイベント。
ハッカソンは1日から一週間の期間で開催することがある。
24時間を越えたり競争が激しい場合は参加者はしばしばピザや栄養ドリンクでくだけた食事をする。また寝る時も寝袋などで雑魚寝する。
コンテスト要素も時々あり審査員が優勝チームを選出し賞を授与する。
by Wikipediaより抜粋

以下、先にネタバレですが、ひとりハッカソンでやったことを簡単にまとめると…

  • 3DCGモデルを探す
  • GLTFLoaderを使って3DCGモデルを読み込む
  • 3DCGオブジェクトのclick(touch)イベント実装
  • CodePenで実装、ライブラリはCDNで

一気に手を進めながらの行き当たりばったりでしたが、割とネタになるような箇所に触れていたり。

部分的に気になる記事がありましたらそちらにお進み下さい。

(一応、今回はひとりハッカソンに参加したというお話なため…)

今日のお題、

12時間以内にクリスマスな3DCGをThree.jsを使ってCodePenで公開する!

それでは…開始!!

プロローグ

2019年12月某日、某所、午前9時にスタートを切る。

会場(自宅)は熱気に包まれ、手に汗握る。

「12時間以内にクリスマスな3DCGをThree.jsを使ってCodePenで公開する」かあ…

他の人は何を作るんだろうか、負けていられない。(いや、ひとりです)

さて、何を作ろう…

3DCGモデルを探す

まず、クリスマスのネタになるモデルを探すべきかなと考えました。

3DCGのモデルをパパっと作れればいいのですが、一度も作ったことがなく、

さすがにそれは難易度が高いので、

パブリックドメインの3DCGのモデルを探すことにしました。

それにしてもどこから探せばいいのやら…

路頭に迷っていたところ、Three.jsの公式ページでglTF(※)を扱っているサイト

をおすすめしているのを知りました。

※glTF (GL Transmission Format) について
glTFはJSONによって3Dモデルやシーンを表現するフォーマットである。「3DにおけるJPEG」と表現されることもある。(by Wikipedia)
可能であれば、glTFの使用をお勧めします。
ランタイムアセット配信に重点を置いているため、送信がコンパクトでロードが高速です。機能には、メッシュ、マテリアル、テクスチャ、スキン、スケルトン、モーフターゲット、アニメーション、ライト、カメラが含まれます。(by Three.js公式ページ)

こんなサイトがあるなんて知らなかった!

いろんな3DCGがある!

思わず見とれてしまう…が、今は時間がない。

とりあえず今日のお題はクリスマスなのでクリスマス的な何かを探す。

サンタさん、雪の景色、雪国の家、雪だるま、暖炉のある部屋…

よし、これにしよう。

GLTFLoaderを使って3DCGモデルを読み込む

ひととおり、Sketchfabで表示する3DCGモデルを選んでダウンロードした後、

次にするのは、そのモデルを読み込む処理を書きます。

こちらもThree.js公式ページを参考にして実装してみました。

公式ページの下のほうに「Troubleshooting」(トラブルシューティング)「Asking for help」(助けを求める)

とあったので、手こずるかな、と思いきや意外とすんなり実装…

まず、「GLTFLoader.js」を読み込みます。

<script src="./node_modules/three/examples/js/loaders/GLTFLoader.js"></script>
// Omit...
var loader = new THREE.GLTFLoader();
loader.load('./model/gltf/winter_feast/scene.gltf', function (gltf) {
 scene.add(gltf.scene);
 // Omit...
)

こんな感じでメインとなるモデルは読み込めたのですが、その後、

「そうだ!間違い探しみたいに、クリスマスとは関係ないものを置いてみよう」

と急にアイデアがひらめき、複数のモデルを読み込むも全く表示されず…

「むむむ」
「複数のモデル読み込みはだめなの?」
「何か特別な実装が必要!?」

とちょっとはまりそうになりかけた時に公式ページに戻って色々試した結果、
私の場合は、

4.Try to add and position a light source. The model may be hidden in the dark.
(4.光源を追加して配置してみてください。モデルは暗闇に隠れている場合があります。)

が原因で、

const light = new THREE.AmbientLight(0xFFFFFF, 1.0);
scene.add(light);

の二行を追加して光をあててみるとあっさり解決しました。

さっきの、

間違い探しみたいに、クリスマスとは関係ないものを置いてみよう

の流れで、

オブジェクトをクリックしたら、なんか吹き出しとか出てオブジェクトが話すの面白くない?

という流れになり、

3DCGオブジェクトのclick(touch)イベントを実装しようと思いました。

ところが、それが悪夢の始まりになるとは…その時は思いもせず。(おおげざ)

どうなる!?ひとりハッカソン!

3DCGオブジェクトのclick(touch)イベント実装

結果的には実装できましたが、色々やって一番時間がかかった箇所です。

そもそも3DCGでのクリックイベントの実装は、Webと同じ感覚で簡単にできるものと思っていました。

ところが、調べていくと3DCGオブジェクトをclick(touch)して何かを実装しようとしたら…

  1. click(touch)したマウスポインターの位置を取得する
  2. マウスポインターから光線を発射する
  3. 光線が当たった(交差した)オブジェクトを抽出する(近い順に格納されている)

光線、交差!?なんじゃそりゃ!?

完全にWebのノリが抜けきれていない私は洗礼を受けました…。

実装には、「Raycaster」というクラスを使います。

var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
// マウスポインタの位置座標の取得
var x = event.clientX ? event.clientX : (event.touches[0] || event.changedTouches[0] || {}).clientX;
var y = event.clientY ? event.clientY : (event.touches[0] || event.changedTouches[0] || {}).clientY;
mouse.x = (x / window.innerWidth) * 2 - 1;
mouse.y = - (y / window.innerHeight) * 2 + 1;
// 光線を発射
raycaster.setFromCamera(mouse, camera);
// 光線と交わるオブジェクトを収集
var intersects = raycaster.intersectObjects(scene.children, true);
// 交わるオブジェクトが1個以上の場合
if (intersects.length > 0) {
    // 実行したい処理をかく
}

マウスポインタの位置座標の取得ですが、スマートフォン等のタッチイベントを利用する場合は、

event.touches[0]またはevent.changedTouches[0]から取得してください。

ここで、重要なのが(私がはまった)sceneのオブジェクトと光線との交差をチェックする呼び出しの「intersectObjects」の第二引数に「true」を指定したことです。

var intersects = raycaster.intersectObjects(scene.children, true);

第二引数は「it also checks all descendants of the objects. 」つまり、

オブジェクトの再帰的チェックをしてくれます。デフォルトはfalseです。(省略可)

恐らく複数のオブジェクトを使っているからか、

またはモデルが親子関係でオブジェクトを実装しているのか、

私の場合、ここをtrueにしないとclick(touch)したオブジェクトを検知できませんでした。

(このRaycasterの実装だけでも1記事書けそう…)

さあ、貴重な時間がどんどん過ぎていきました。

これで大体は実装できたかな?

あとは吹き出しっと…

CSSのサンプルを探していたら、たまたまドラクエ風コマンドデザインが目に留まる。

私の脳内で一気に堀井雄二(※)ワールドがさく裂…

(堀井雄二の世界が好きなんです。ファミコンの…)

吹き出しのことは忘れた。

「よし、これでいこう!」

とやりたいことの方針が大体固まる。

(ちなみに今はやりのDQウォークはまだやったことない)

※堀井雄二

ゲームデザイナー、シナリオライター、ドラゴンクエストシリーズの生みの親。

CodePenで実装、ライブラリはCDNで

とりあえず、自分のローカル環境に「main.html」を書いてそこで動かしていたけど、

これをCodePenで公開するのが今回のひとりハッカソンのゴール。

CodePenで公開するためには、ちょっとひと工夫が必要。

CodePenだけで完結しないといけなく、例えば、外部読み込みのライブラリとか。

ライブラリはCDNを使って呼び出す

今回、実装にあたり「Three.js」「GLTFLoader.js」「OrbitControls.js」のライブラリを使用。

ローカル環境ではnpm installで落としてきたものを使っていたが、

https://cdnjs.cloudflare.com/ajax/libs/three.js/109/three.min.js

のようにCDNが存在していた為、これを使う。他のライブラリも同様に。

ここで気をつけたいのが、

https://cdn.jsdelivr.net/gh/mrdoob/three.js@r109/examples/js/loaders/GLTFLoader.js

のようにバーションを合わせて使うこと。

現在、公式ページでは「r111」が最新だが、CDNだと「109」が最新なため、このバージョンで合わせることにする。

3DCGモデルの置き場についてはhostingサービスを利用しました。
こちらについては割愛させて頂きます。(CORS対応を忘れずに…)

さあ、後はbodyのinfo領域に、ダウンロードした3DCGのモデル情報を記載して、いざ公開。

くれぐれもキャンドルは熱いので触らないでください(笑)

Full表示はこちら

エピローグ

「終了!」

その時、ハッカソン終了の合図が会場(自宅)に鳴り響いた。

ふう、なんとか間に合ったようだ。

「特別参加賞はsgi-changです」

初めてのひとりハッカソンで受賞だなんて…泣けてくる。

もちろん、さびしいんじゃない、嬉しくて。

このハッカソンで色々と得るものがあったなあ、としみじみ。

次はUnity、VRにも挑戦したい。(もちろん、UXDもからめて…)

また来年も頑張るぞ!と意気込むsgi-changであった。

ここまでお読み頂き、誠にありがとうございました。

貴重なお時間をすみません。

最後にこれでしめたいと思います。(ちょっとやってみたかった。(笑))

※何も表示されていない場合、右下の「Return」を押してください。