Paranomic Ball Contents Viewer
2015.05.08
Category
Tags
最近、全天球コンテンツに興味があるので、Three.jsで全天球画像(動画)ビューワを作ってみました。
全天球画像ビューワ自体は簡単に作れました。
要は球を作ってEquirectangular形式(正距円筒図法)の画像を球の内側に貼り付けるだけ。
ソースコードはこちら。
実装方法はいろいろ転がってましたが、球の内側にマウスのインタラクションを付けたかったので、現在の形に落ち着きました。
ちなみに、スマホでも見れますが、Androidは未検証です。多分重い。
iOS(ver.8以上)では割と軽快に動きますが、やはりVideo問題が。
iOSではビデオを再生するときは、ユーザーが明確にクリックなどで再生のスタートをさせないと、動画が再生できない上、iPhone/iPod touchの場合は全画面で動画が立ち上がります。
こちらのサンプルはPCでは問題無いですが、iOSでは上にある「play」ボタンをクリックしないと動画が再生されません。また、iPhone/iPod touchでは、再生されても全画面で立ち上がり、再生をストップした時点でその状態の画像がTextureとしてCubeに反映されます。
要はiOSで全天球動画をHTMLで見ることはできない!(はず。)
Androidではそもそも画像が表示されなかったり、画像は表示されても動画がiOSと同じく再生されなかったりと、いろいろ問題があります。
球の内側のマウスオーバー(スマホはスワイプ時)のインタラクションですが、Three.jsのRaycasterクラスを使用して実現しています。
Three.jsのRaycasterを使って、画面内のオブジェクトを得る
上記の記事と、Raycasterクラスのドキュメントを参考にしました。
Raycasterの考え方はこちらを参照 (ただし古いProjecterクラスを利用した実装方法です)。
球の内側にインタラクションを付けたかったのですが、そのためにはMaterialのオプションに
side: THREE.BackSide
を指定する必要があったので、GeometryやMeshを反転させるのではなく、Textureを反転させて貼り付ける実装方法を採用しました。
以下、クラスメソッドを抜粋したものです (CoffeeScript)。※一部省略
# テクスチャ生成 createTexture: (src, width, height)=> canvas = document.createElement 'canvas' canvas.width = width canvas.height = height ctx = canvas.getContext '2d' ctx.translate width, 0 ctx.scale -1, 1 ctx.drawImage src, 0, 0, width, height texture = new THREE.Texture canvas
srcはimgやvideo、widthとheightはそれぞれsrcの幅と高さです。
videoの場合は描画更新時に都度ctx.drawImageする必要はありますが、基本同じです。
マウスオーバーのインタラクションはRaycasterのintersectObjectメソッドで取得できるface (球のGeometryのface)の色をランダムで変更しています。以下クラスメソッドの抜粋です。
@sphereGeometryは球のGeometry、@canvasElmは描画しているcanvasエレメント、@imgSphereは球のMeshです。
また、mouseMoveHandler、getRaycasterIntersectsの引数はマウスのx, y座標を入れます。
アニメーションのライブラリはTweenMaxを使用しています。
# mouse move (touch move) mouseMoveHandler: (clientX, clientY)=> intersects = @getRaycasterIntersects clientX, clientY # 交差しているMeshがあれば、intersectsの配列に格納される if intersects.length > 0 intersect = intersects[0] color = @sphereGeometry.faces[intersect.faceIndex].color TweenMax.killTweensOf color TweenMax.to color, 0.2, { r: Math.random() * 6, g: Math.random() * 6, b: Math.random() * 6, ease: Sine.easeOut, onUpdate: => @sphereGeometry.colorsNeedUpdate = true } TweenMax.to color, 0.6, { r: 1, g: 1, b: 1, ease: Sine.easeInOut, delay: 0.2, onUpdate: => @sphereGeometry.colorsNeedUpdate = true }
# インタラクション判定 getRaycasterIntersects: (clientX, clientY)=> # スクリーン上のマウス位置を取得する mouse = new THREE.Vector2() mouse.x = (clientX / @canvasElm.width) * 2 - 1 mouse.y = -(clientY / @canvasElm.height) * 2 + 1 raycaster = new THREE.Raycaster() raycaster.setFromCamera mouse, @camera # 交差判定 # 引数は取得対象となるMeshの配列を渡す。(子要素も対象とする場合は第二引数にtrueを指定する) return raycaster.intersectObject @imgSphere, true
次に、画像の切り替えのトランジションです。
最初はGeometryの各頂点が球の中心に直線的に収束していくものを作ったんですが、微妙でした。そこで、球上のクリックした位置に頂点が収束するようにしました。かつ、直線的にではなく、球の表面上を沿ってアニメーションします。
考え方としては、各頂点を原点が球の中心のベクトルとし、クリックした点まで回転運動させるイメージです。
各頂点もクリックした点も球上の点なので、ベクトルの長さは同じです。よって回転運動だけでOKです。
簡単に説明すると、2つのベクトルが作る平面の法線ベクトルを軸として、クォータニオンを使って回転させます。
以下、クラスメソッドの抜粋です。
_POINT_ZEROは原点(球の中心)です。メソッドの引数のindexは頂点のインデックス番号、fromは移動前の座標(Vector3)、toは移動後の座標(Vector3)です。出現時と収束時で頂点のアニメーション開始の時間(delay)を調整しているため、その判断をするためにshowという引数(真偽値)を設けています。
# クォータニオン回転 quaternionRotate: (index, from, to, show = true)=> # 回転の軸となる法線ベクトルを求める normal = _POINT_ZERO.clone() normal.crossVectors(from, to).normalize() # 角度を求める theta = Math.acos(from.dot(to) / (from.distanceTo(_POINT_ZERO) * to.distanceTo(_POINT_ZERO))) if show delay = (Math.PI - Math.abs(theta)) / Math.PI else delay = Math.abs(theta) / Math.PI tweenObj = { r: 0 } return TweenMax.to tweenObj, 0.6 + delay, { r: theta delay: delay ease: Expo.easeInOut onUpdate: => # 4x4の回転行列を掛けるので、ベクトルも4x1に vector4 = new THREE.Vector4 from.x, from.y, from.z, 1 # 法線ベクトルと角度からクォータニオンを生成 quaternion = new THREE.Quaternion() quaternion.setFromAxisAngle normal, tweenObj.r # 生成したクォータニオンから回転行列を生成、ベクトルに掛ける matrix = new THREE.Matrix4() matrix.makeRotationFromQuaternion quaternion vector4.applyMatrix4 matrix # 変形後の頂点を反映 @sphereGeometry.vertices[index].set vector4.x, vector4.y, vector4.z @sphereGeometry.verticesNeedUpdate = true }
さすが3Dのライブラリ。クォータニオンから回転行列の算出とかも一発です。楽ちん。
全天球コンテンツは3Dと絡められるので楽しいですね。
ただ、やっぱりAndroidは面倒です。。