2018/07/11
Three.jsで360°パノラマ動画を表示する
Three.js を使うと、簡単に WebGL による 3D の描画ができ、360°パノラマのビューアーを作ることもできます。
これは、球体に360°のパノラマ画像をテクスチャとして設定することで実現していますが、
Three.js ではこのテクスチャに動画を指定することもできるので、
360°動画のプレイヤーを作ることも比較的簡単にできてしまいます。
公式の DEMO でも見ることができますが、
こちらは VR のみで操作可能で、ドラッグやスワイプでの操作はできないようでしたので、
ドラッグやスワイプで操作できるものを作ってみました。
↓ 作ってみたもの
↓こちらの360°のパノラマ動画を使わせていただきました。
Free 360 Video Downloads Page | 360/VR Master Series | Mettle
作り方
HTML で Three.js の読み込みと要素の準備
1 2 3 | <div id="container"></div> <script src="js/three.min.js"></script> |
HTML で動画を配置する場所を用意し、
公式サイトからダウンロードした Three.js のファイルをサーバーに配置し、読み込みます。
video 要素を JavaScript で生成し、動画のテクスチャを生成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // video を設定 video = document.createElement( 'video' ); video.crossOrigin = 'anonymous'; video.loop = true; video.muted = true; video.src = 'textures/video4.mp4'; video.setAttribute( 'webkit-playsinline', 'webkit-playsinline' ); video.setAttribute( 'playsinline', 'playsinline' ); video.setAttribute( 'muted', 'muted' ); video.play(); // video からテクスチャを生成 texture = new THREE.Texture( video ); texture.generateMipmaps = false; texture.minFilter = THREE.NearestFilter; texture.maxFilter = THREE.NearestFilter; texture.format = THREE.RGBFormat; // 動画に合わせてテクスチャを更新 setInterval( function () { if ( video.readyState >= video.HAVE_CURRENT_DATA ) { texture.needsUpdate = true; } }, 1000 / 24 ); |
JavaScript 上で video 要素を生成します。playsinline 属性を指定するのがポイントとなり、これにより、全画面で動画が再生されてしまうのを防ぐことができます。
video 要素を生成したら、その video 要素からテクスチャを生成します。
そのままではテクスチャが静止画になってしまうので、setInterval で動画の状態に合わせてテクスチャを更新するように設定します。
球体にテクスチャを設定し、シーンに追加
1 2 3 4 5 6 7 8 9 10 11 | // カメラを生成 camera = new THREE.PerspectiveCamera( 75, container.innerWidth / container.innerHeight, 1, 2000 ); // シーンを生成 scene = new THREE.Scene(); // 球体を作成し、テクスチャに video を元にして生成したテクスチャを設定します var geometry = new THREE.SphereBufferGeometry( 500, 60, 40 ); geometry.scale( - 1, 1, 1 ); var mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { map: texture } ) ); scene.add( mesh ); |
球体をカメラの周りを囲むように設定し、テクスチャとして、先程の動画のテクスチャを指定します。
レンダリング、ドラッグ・スワイプ操作の設定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | // レンダラーを生成 renderer = new THREE.WebGLRenderer(); renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); container.appendChild( renderer.domElement ); // ドラッグ・スワイプ操作を設定 container.addEventListener( EVENT.TOUCH_START, onDocumentMouseDown, false ); // 画面のリサイズに対応 window.addEventListener( 'resize', onWindowResize, false ); onWindowResize( null ); } function onWindowResize ( event ) { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize( window.innerWidth, window.innerHeight ); } function onDocumentMouseDown( event ) { event.preventDefault(); if(event.clientX) { onMouseDownMouseX = event.clientX; onMouseDownMouseY = event.clientY; } else if(event.touches) { onMouseDownMouseX = event.touches[0].clientX onMouseDownMouseY = event.touches[0].clientY; } else { onMouseDownMouseX = event.changedTouches[0].clientX onMouseDownMouseY = event.changedTouches[0].clientY } onMouseDownLon = lon; onMouseDownLat = lat; document.addEventListener( EVENT.TOUCH_MOVE, onDocumentMouseMove, false ); document.addEventListener( EVENT.TOUCH_END, onDocumentMouseUp, false ); } function onDocumentMouseMove( event ) { event.preventDefault(); if(event.clientX) { var touchClientX = event.clientX; var touchClientY = event.clientY; } else if(event.touches) { var touchClientX = event.touches[0].clientX var touchClientY = event.touches[0].clientY; } else { var touchClientX = event.changedTouches[0].clientX var touchClientY = event.changedTouches[0].clientY } lon = ( touchClientX - onMouseDownMouseX ) * -0.15 + onMouseDownLon; lat = ( touchClientY - onMouseDownMouseY ) * -0.15 + onMouseDownLat; } function onDocumentMouseUp( event ) { document.removeEventListener( EVENT.TOUCH_MOVE, onDocumentMouseMove, false ); document.removeEventListener( EVENT.TOUCH_END, onDocumentMouseUp, false ); } function animate() { renderer.setAnimationLoop( render ); } function render() { lat = Math.max( - 85, Math.min( 85, lat ) ); phi = THREE.Math.degToRad( 90 - lat ); theta = THREE.Math.degToRad( lon ); camera.position.x = 100 * Math.sin( phi ) * Math.cos( theta ); camera.position.y = 100 * Math.cos( phi ); camera.position.z = 100 * Math.sin( phi ) * Math.sin( theta ); camera.lookAt( scene.position ); renderer.render( scene, camera ); } |
あとはドラッグ・スワイプの操作を設定して、レンダリングすれば完成です。
このように Three.js では、依然ご紹介したことのある静止画の 360°ビューアーを作るときとほぼ同じ手順で、360°動画のビューアーを作ることができます。
オリジナルの360°動画ビューアーが必要な場合には、とても重宝しそうです。
JavaScript コード全文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | "use strict"; //イベントの振り分け var EVENT = {}; if ('ontouchstart' in window) { EVENT.TOUCH_START = 'touchstart'; EVENT.TOUCH_MOVE = 'touchmove'; EVENT.TOUCH_END = 'touchend'; } else { EVENT.TOUCH_START = 'mousedown'; EVENT.TOUCH_MOVE = 'mousemove'; EVENT.TOUCH_END = 'mouseup'; } (function () { // 変数の初期化 var camera, scene, renderer, video, texture, container; var fov = 60, isUserInteracting = false, onMouseDownMouseX = 0, onMouseDownMouseY = 0, lon = 0, onMouseDownLon = 0, lat = 0, onMouseDownLat = 0, phi = 0, theta = 0; init(); animate(); function init() { // コンテナの準備 container = document.getElementById( 'canvas-frame' ); container.addEventListener( 'click', function () { video.play(); } ); var select = document.getElementById( 'video_src' ); select.addEventListener( 'change', function (e) { video.src = select.value; video.play(); } ); // video 要素を生成 video = document.createElement( 'video' ); video.crossOrigin = 'anonymous'; video.loop = true; video.muted = true; video.src = 'textures/video4.mp4'; video.setAttribute( 'webkit-playsinline', 'webkit-playsinline' ); video.setAttribute( 'playsinline', 'playsinline' ); video.setAttribute( 'muted', 'muted' ); video.play(); // video からテクスチャを生成 texture = new THREE.Texture( video ); texture.generateMipmaps = false; texture.minFilter = THREE.NearestFilter; texture.maxFilter = THREE.NearestFilter; texture.format = THREE.RGBFormat; // 動画に合わせてテクスチャを更新 setInterval( function () { if ( video.readyState >= video.HAVE_CURRENT_DATA ) { texture.needsUpdate = true; } }, 1000 / 24 ); // カメラを生成 camera = new THREE.PerspectiveCamera( 75, container.innerWidth / container.innerHeight, 1, 2000 ); // シーンを生成 scene = new THREE.Scene(); // 球体を作成し、テクスチャに video を元にして生成したテクスチャを設定します var geometry = new THREE.SphereBufferGeometry( 500, 60, 40 ); geometry.scale( - 1, 1, 1 ); var mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { map: texture } ) ); scene.add( mesh ); // レンダラーを生成 renderer = new THREE.WebGLRenderer(); renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); container.appendChild( renderer.domElement ); // ドラッグ・スワイプ操作を設定 container.addEventListener( EVENT.TOUCH_START, onDocumentMouseDown, false ); // 画面のリサイズに対応 window.addEventListener( 'resize', onWindowResize, false ); onWindowResize( null ); } function onWindowResize ( event ) { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize( window.innerWidth, window.innerHeight ); } function onDocumentMouseDown( event ) { event.preventDefault(); if(event.clientX) { onMouseDownMouseX = event.clientX; onMouseDownMouseY = event.clientY; } else if(event.touches) { onMouseDownMouseX = event.touches[0].clientX onMouseDownMouseY = event.touches[0].clientY; } else { onMouseDownMouseX = event.changedTouches[0].clientX onMouseDownMouseY = event.changedTouches[0].clientY } onMouseDownLon = lon; onMouseDownLat = lat; document.addEventListener( EVENT.TOUCH_MOVE, onDocumentMouseMove, false ); document.addEventListener( EVENT.TOUCH_END, onDocumentMouseUp, false ); } function onDocumentMouseMove( event ) { event.preventDefault(); if(event.clientX) { var touchClientX = event.clientX; var touchClientY = event.clientY; } else if(event.touches) { var touchClientX = event.touches[0].clientX var touchClientY = event.touches[0].clientY; } else { var touchClientX = event.changedTouches[0].clientX var touchClientY = event.changedTouches[0].clientY } lon = ( touchClientX - onMouseDownMouseX ) * -0.15 + onMouseDownLon; lat = ( touchClientY - onMouseDownMouseY ) * -0.15 + onMouseDownLat; } function onDocumentMouseUp( event ) { document.removeEventListener( EVENT.TOUCH_MOVE, onDocumentMouseMove, false ); document.removeEventListener( EVENT.TOUCH_END, onDocumentMouseUp, false ); } function animate() { renderer.setAnimationLoop( render ); } function render() { lat = Math.max( - 85, Math.min( 85, lat ) ); phi = THREE.Math.degToRad( 90 - lat ); theta = THREE.Math.degToRad( lon ); camera.position.x = 100 * Math.sin( phi ) * Math.cos( theta ); camera.position.y = 100 * Math.cos( phi ); camera.position.z = 100 * Math.sin( phi ) * Math.sin( theta ); camera.lookAt( scene.position ); renderer.render( scene, camera ); } })(); |
Author Profile
NINOMIYA
Webデザイナー兼コーダー出身のフロントエンド開発者です。 UXデザインやチーム開発の効率化など、勉強中です。
SHARE