Three.jsで360度パノラマコンテンツ制作

VRを体験してみると、やはり多いのが360度パノラマ画像を使用したコンテンツです。そこで360度パノラマコンテンツを制作したいと思います。A-FRAMEを使用するともっと簡単にできると思いますが、いろいろ試していきたいので、Three.jsを使用して制作します。
※Three.jsの仕様が変更になったため内容を修正しました。(2019年12月12日)
360度パノラマコンテンツ制作
● 写真の用意
まずは写真を用意する必要があります。RICOH THETAなどの360度カメラを使用すれば撮影できますが、今回はFlickr VRでダウンロードさせてもらいました。
トップページのExplore VR photosから好きな写真を選んで、写真のページへ移動し、右下のアイコンから画像をダウンロードします。Oculus Goで動作確認をして、読み込む画像のサイズは、4096px×2048pxくらいがちょうどうよかったので、フォトショップで解像度を調整しました。
● 仕組み
360度パノラマ制作に関する情報はいろいろとありますが、「お手軽360°パノラマ制作入門!JSでパノラマビューワーを自作しよう」がわかりやすかったので参考にさせてもらいました。
仕組みはシンプルで、3D空間上に球体を配置し、裏側に360度パノラマ写真のテクスチャをはります。あとは3D空間の中心にカメラを設置し視点操作をするだけです。
Three.jsの基本は、いろいろなサイトに情報が載っているので省略しますが、「Three.js」の中の「examples > js」と「examples > jsm」にOrbitControls.jsなど様々な拡張機能用のライブラリーが入っています。※「jsm」はES2015(ES6)用です。
● script.js
Three.js関連のライブラリはscript.jsからインポートするので、script.jsはtype="module"をつけて読み込みます。
<script src="js/script.js" type="module"></script>
視点操作がコントロールできる360度パノラマコンテンツを制作します。OrbitControlsは、カメラのポジションを(0,0,0)にすると動かないので注意が必要です。
※OrbitControls.js内のthree.module.jsのパスを変更する必要があります。
//===============================================================
// Import Library
//===============================================================
import * as THREE from './lib/three_jsm/three.module.js';
import { OrbitControls } from './lib/three_jsm/OrbitControls.js';
import { VRButton } from './lib/three_jsm/VRButton.js';
//===============================================================
// Main
//===============================================================
window.addEventListener('load',function(){
init();
});
let scene,camera,renderer;
function init(){
//シーン、カメラ、レンダラーを生成
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(90,window.innerWidth/window.innerHeight,0.1,1000);
camera.position.set(0,1.6,3);
scene.add(camera);
renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth,window.innerHeight);
renderer.setClearColor(new THREE.Color(0x000000));
//canvasを作成
const container = document.createElement('div');
document.body.appendChild(container);
container.appendChild(renderer.domElement);
//球体の形状を生成
const geometry = new THREE.SphereGeometry(100,100,100);
geometry.scale(-1,1,1);
//テクスチャ画像を読み込み
const loader = new THREE.TextureLoader();
const texture = loader.load('./img/pict.jpg');
//球体のマテリアルを生成
const material = new THREE.MeshBasicMaterial({
map:texture
});
//球体を生成
const sphere = new THREE.Mesh(geometry, material);
scene.add(sphere);
//OrbitControlsを初期化
const orbitControls = new OrbitControls(camera,renderer.domElement);
render();
}
function render(){
requestAnimationFrame(animate);
}
function animate(){
renderer.render(scene,camera);
}
● ウィンドウのリサイズに対応
ウィンドウのリサイズに対応します。
window.addEventListener('resize',function(){
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth,window.innerHeight);
},false);
● ローディング画面を設置
ローディング画面をつけます。データの読み込み処理はPreloadJSを使用します。ローディング画面は今回のテーマではないので詳細は省略しますが、下記Qiitaの記事がわかりやすいです。また、ローディングアニメーションは、「Single Element CSS Spinners」を使用させてもらいました。
preloadjs.min.jsを読み込みます。TweenMax.min.jsはローディング画面のアニメーションに使用しています。
<script src="js/preloadjs.min.js"></script> <script src="js/TweenMax.min.js"></script> ~ 略 ~~ 略 ~Loading...
これで、ローディングアニメーション後にパノラマ画像が表示されるようになります。
setLoading();
let texture;
function setLoading(){
TweenMax.to('.loader',0.1,{opacity:1});
const manifest = [
{id:'pict01',src:'./img/pict.jpg'}
];
const loadQueue = new createjs.LoadQueue();
loadQueue.on('progress',function(e){
const progress = e.progress;
});
loadQueue.on('complete',function(){
const image = loadQueue.getResult('pict01');
//テクスチャは画像を読み込んだ後に設定
texture = new THREE.Texture(image);
texture.needsUpdate = true;
TweenMax.to('#loader_wrapper',1,{
opacity:0,
onComplete:function(){
document.getElementById('loader_wrapper').style.display ='none';
}
});
//ローディング後実行
threeWorld();
});
loadQueue.loadManifest(manifest);
}
function threeWorld(){
~ 略 ~
//球体のマテリアルを生成
const material = new THREE.MeshBasicMaterial({
map:texture
});
~ 略 ~
}
● WebVRに対応
最後にWebVRに対応します。Three.jsは頻繁にアップデートされていて、2019年12月時点ではr111ですが、WebVR.jsが非推奨になり、代わりにVRButton.jsを使用するように仕様が変更されました。詳細は下記の記事を参考にしてください。
「renderer.xr.enabled」でVRを許可し、VRButton.jsを使用して 「ENTER VR」ボタンを追加します。VRButton.jsはWebXR Device APIを使用していて、WebXR Device APIに対応していないブラウザは、「WEBXR NOT SUPPORTED」と表示されます。最後にrequestAnimationFrameをsetAnimationLoopに変更します。
~ 略 ~
function init(){
~ 略 ~
renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth,window.innerHeight);
renderer.setClearColor(new THREE.Color(0x000000));
//VRを許可
renderer.xr.enabled = true;
const container = document.querySelector('#canvas_vr');
container.appendChild(renderer.domElement);
//VRボタンを設置
document.body.appendChild(VRButton.createButton(renderer));
~ 略 ~
}
function render() {
//setAnimationLoopに変更
//requestAnimationFrame(animate);
renderer.setAnimationLoop(animate);
}
function animate(){
renderer.render(scene,camera);
}
● script.js
ソースコードを整理して、完成したscript.jsになります。
//===============================================================
// Import Library
//===============================================================
import * as THREE from './lib/three_jsm/three.module.js';
import { OrbitControls } from './lib/three_jsm/OrbitControls.js';
import { VRButton } from './lib/three_jsm/VRButton.js';
//===============================================================
// Main
//===============================================================
window.addEventListener('load',function(){
init();
});
let scene,camera,renderer;
let texture;
let orbitControls;
function init(){
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(90,window.innerWidth/window.innerHeight,0.1,1000);
camera.position.set(0,1.6,3);
scene.add(camera);
renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth,window.innerHeight);
renderer.setClearColor(new THREE.Color(0x000000));
const container = document.querySelector('#canvas_vr');
container.appendChild(renderer.domElement);
document.body.appendChild(VRButton.createButton(renderer));
window.addEventListener('resize',function(){
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth,window.innerHeight);
},false);
checkDevice();
setLoading();
}
function checkDevice(){
if ('xr' in navigator) {
navigator.xr.isSessionSupported('immersive-vr').then(function(supported){
if(supported){
renderer.xr.enabled = true;
}else{
setController();
}
});
} else {
setController();
}
}
function setLoading(){
TweenMax.to('.loader',0.1,{opacity:1});
const manifest = [
{id:'pict01',src:'./img/pict.jpg'}
];
const loadQueue = new createjs.LoadQueue();
loadQueue.on('progress',function(e){
const progress = e.progress;
});
loadQueue.on('complete',function(){
const image = loadQueue.getResult('pict01');
texture = new THREE.Texture(image);
texture.needsUpdate = true;
TweenMax.to('#loader_wrapper',1,{
opacity:0,
onComplete:function(){
document.getElementById('loader_wrapper').style.display ='none';
}
});
threeWorld();
rendering();
});
loadQueue.loadManifest(manifest);
}
function threeWorld(){
const geometry = new THREE.SphereGeometry(100,100,100);
geometry.scale(-1,1,1);
const material = new THREE.MeshBasicMaterial({
map:texture
});
const sphere = new THREE.Mesh(geometry,material);
scene.add(sphere);
}
function setController(){
document.addEventListener('touchmove',function(e){e.preventDefault();},{passive:false});
orbitControls = new OrbitControls(camera,renderer.domElement);
orbitControls.target.set(0,1.6,0);
orbitControls.enableDamping = true;
orbitControls.dampingFactor = 0.5;
orbitControls.enableZoom = false;
}
function rendering(){
renderer.setAnimationLoop(animate);
}
function animate(){
if(orbitControls){
orbitControls.update();
}
renderer.render(scene,camera);
}
完成したデモになります。ヘッドマウントディスプレイで見ると、ルーブル美術館にいる気分が味わえます!

