ウェブブラウザでパノラマ画像を手軽に表示するには、Panolens.jsとThree.jsの組み合わせが便利です。
ただ解像度の高いパノラマ画像はファイルサイズも巨大なため、何枚も同時に表示させるとデータの転送量が大きくなってしまいます。
そこでビューア画面をクリックしたら、その都度パノラマ画像が読み込まれるように工夫してみました。
この方法であれば、8Kサイズの巨大パノラマ画像をウェブページに何枚も埋め込むことが可能です。
(Windows11とGoogle ChromeおよびMicrosoft Edgで動作確認済み)
上記のデモに使っているパノラマ画像は、ILLUST55様のウェブサイトからお借りしました。
画像の説明テキストも、記載されていたタイトルをそのまま表示させていただきました。
サンプルコード
<!DOCTYPE html>
<html lang="ja">
<head> <meta charset="UTF-8" /> <link rel="stylesheet" href="PanoramaStyle.css" />
</head>
<body> <div id="0" class="pano" onclick="panoView(this.id);"></div> <div id="1" class="pano" onclick="panoView(this.id);"></div> <div id="2" class="pano" onclick="panoView(this.id);"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/105/three.js"></script> <script src="https://cdn.jsdelivr.net/npm/panolens@0.11.0/build/panolens.min.js"></script> <!--オプション--> <!--<script src="https://rawgit.com/pchen66/panolens.js/dev/build/panolens.min.js"></script>--> <script src="PanoramaPlayer.js"></script>
</body>
</html>
@charset "UTF-8";
body { margin-left: 20px; margin-right: 20px; line-height: 2.0em; font-family: sans-serif;
}
.pano{ background-repeat:no-repeat; background-size: 100%; background-position: center; height: 500px; width: 100%; /*aspect-ratio: 2/1;*/
}
@media (min-width:1040px) {body{width: 1000px;}}
@media (max-width:1000px) {.pano{height: 500px; background-size: 150%;}}
@media (max-width:900px) {.pano{height: 450px;}}
@media (max-width:800px) {.pano{height: 400px;}}
@media (max-width:700px) {.pano{height: 350px;}}
@media (max-width:600px) {.pano{height: 300px;}}
@media (max-width:500px) {.pano{height: 250px;}}
@media (max-width:400px) {.pano{height: 200px;}}
//操作説明透過画像
const bgImage="ClickToPlay.png";
//パノラマ関連配列[背景画像][パノラマ画像][タイトル][表示済みフラグ]
const panoInfo=[ ["bg-sea.jpg", "pano-sea.jpg", "綺麗な海と山と青い海&西洋の小屋", false], ["bg-snow.jpg", "pano-snow.jpg", "リアルな雪山 & 晴れた昼のキレイな景色", false], ["bg-town.jpg", "pano-town.jpg", "ファンタジーの中世ヨーロッパの夜の街", false]
];
//背景画像設定
for(let i=0; i<panoInfo.length; i++)
{ let elem = document.getElementById(i); elem.style.backgroundImage = "url(" + bgImage + "), url(" + panoInfo[i][0] + ")"; //タイトル表示 let titleText = document.createElement("p"); titleText.textContent = panoInfo[i][2]; elem.before(titleText);
}
//パノラマ画像表示
function panoView(id) { if(panoInfo[id][3]) return; //表示済みならスキップ else{ panoInfo[id][3]=true; let panoImage = new PANOLENS.ImagePanorama(panoInfo[id][1]); let panoViewer = new PANOLENS.Viewer({ container: document.getElementById(id), cameraFov: 60, //カメラ視野角 autoRotate: true, //自動回転 autoRotateSpeed: 0.2, //回転速度 autoRotateActivationDuration: 2000, //自動回転再開までの時間 controlButtons: ['fullscreen'], //フルスクリーンのみ有効化 //視点初期方向(無効?) //initialLookAt: // new THREE.Vector3( Number.MAX_SAFE_INTEGER , 0, 0) }); panoViewer.add(panoImage); }
}
HTMLの説明
パノラマ画像を表示させたい場所にdivタグを設置します。
idは何でもよいのですが、のちほど複数枚のパノラマ画像を自動処理するため、単純に連番数字(0, 1, 2,…)としました。
数字のidが気持ち悪ければ、適当な接頭辞(例 pano-image0, pano-image1,…)を付けてもよいかと思います。
その場合は正規表現などを使って、文字列から数値だけ抽出する必要があります。
document.getElementById()の引数はidに対応する文字列ですが、String()を使わずインデックスの数値をそのまま入れても動きました。
パノラマ画像の遅延読み込み
サムネールのdivの領域をクリックするとpanoView()にidを渡して、パノラマ画像を表示させるという流れになっています。
最初は何も考えず、ページ読み込み時にPanolensで複数画像を配置していました。
しかし8192×4096サイズを数枚読み込ませると、重すぎてブラウザが固まりました。
調べてみると、その際に使ったパノラマは外の風景を撮影した実写画像で、1枚5MB近くありました。
そこで最初は小サイズのプレビュー画像を表示させておいて、ユーザがクリックするとその都度、高解像度のパノラマ画像が読み込まれるように変更しました。
この方法であれば、5MB×10枚程度のパノラマ画像を配置しても、問題なく動作します。
ただし単体でも5MBの画像はさすがに重いので、各パノラマを読み込む際に、通信環境によっては数秒のタイムラグが発生します。
画面の可視領域に応じて画像を先に読み込んでおく、lazyload.jsのような仕組みを併用できれば、さらにスムーズに動くはずです。
パノラマ画像は画質や解像度が上がるほど、ファイルサイズも巨大になっていく宿命にあります。
ウェブで何枚も表示させたい場合は、地味ながら遅延読み込みの処理が必要になってくると思われます。
JavaScriptの説明
Three.jsとPanolens.jsはCDNから読み込んでいます。
それぞれjsファイルをダウンロードしてローカル環境でも動かしてみたのですが、ライブラリのバージョン・組み合わせによって挙動が不安定でした。
ウェブ上のサンプルコードでよく見かける以下の組み合わせであれば、無難に動くと思います。
- Three.js: r105
- Panolens.js: v0.11.0
多次元配列に情報集約
パノラマ画像とサムネール画像(背景画像)、および説明文や表示済みフラグは、panoInfo[]という配列にまとめています。
それぞれ個別に配列を用意したほうがコードの可読性は高まるように思いますが、何となく多次元配列に集約してしまいました。
ファイル名や説明文を変更したり、画像の並び順を変えたりしたい場合は、この配列の中身を編集すれば反映できます。
将来的にパノラマ画像を差し替えてもHTMLファイルはいじらなくて済むよう、pタグと画像タイトルもスクリプトから生成しています。
省力化できる代わりに柔軟な編集はできないので、一長一短といえます。
panoInfo[] 内のbg-○○.jpgというファイルは、パノラマ再生前に表示させておくプレビュー画像です。
これに操作説明の透過画像を重ねて、div要素のbackgroundImageに設定しています。
またパノラマ画像が表示されたかどうかチェックしておかないと、onclickで呼ばれるたびにビューアがいくつも追加されてしまいます。
そこでpanoInfo[]のなかに、表示済み確認用のフラグも含めておきました。
Viewerの設定オプション
Viewerクラスのコンストラクタには、いくつかパラメータが用意されています。
これがパノラマ画像であることをユーザにアピールするため、自動回転はオンにしておいたほうがいいと思います。
画面が変化しないと普通の画像だと思われて、インタラクティブに操作せず見過ごされてしまうおそれがあります。
Viewerでは画像(カメラ)の回転速度はもとより、いったん手動操作して自動回転を中断してから、再開するまでのタイミングも設定できるのが便利です。
controlButtonsはビューア右下に現れる操作アイコンで、デフォルトでは[‘fullscreen’, ‘setting’, ‘video’]の3種が設定されています。
videoというのはPCでもスマホでも表示されなかったので、どういう機能なのかよくわかりません。
settingを有効化すると、画面右下の歯車アイコンが表示され、以下の設定を行うことができます。
- Control: Mouse/Touch/Sensor(視点操作方法)
- Mode: Cardboard/Stereoscopic(画像表示方法)
Modeで表示方法を変更すると、ハコスコのような簡易ゴーグル向けに、左右別れた表示になります。
Stereoscopicだと画像周囲の歪みがなくなります。
どちらの両眼表示モードも、左右で若干視差がついているようです。
ただ実際にゴーグルを当てて試していないので、実際に立体視できるのかは不明です。
iOSでセンサ連動させる方法
iPhoneでControlをSensorに設定しても、真下を向くだけで端末の向きにカメラは反応しませんでした。
最近のiOSではジャイロなどの内蔵センサを使うには、ユーザに許可を求める必要があるようです。
いろいろ調べたところ、Studio Amelieさんがご説明されている方法を試したら、iPhoneでセンサ連動させることができました。
わざわざ自分の体を回転させてパノラマ画像を閲覧する人がいるのかわかりませんが、人前で見せるときには使える演出だと思います。
スマホで全画面表示できない
今回はそこまで凝った使い方は想定していないので、最終的にsettingは非表示にして、fullscreenのオプションだけ残しました。
高解像度のパノラマ画像で没入感を楽しむには、4Kクラスの大型モニタで全画面表示できると便利です。
しかしPCでは全画面表示されますが、iPhoneではそもそもfullscreenのアイコンが表示されませんでした。
これもセンサ利用と同様に、なにかしらコードの追加が必要なのかもしれません。
スタイルシートの説明
CSSはサムネール画像の縦横比を維持しつつ、レスポンシブ対応するのに苦労しました。
アスペクト比の維持
パノラマビューアは横2:縦1の比率にしたかったので、div領域に
aspect-ratio: 2/1;
と記述すれば一発で済むはずです。
ところがパノラマ画像をいったんフルスクリーン表示してから解除すると、高さだけ拡大されたまま元に戻らないという問題(バグ?)に気づきました。
試行錯誤しましたが、結局widthは100%指定のまま、メディアクエリのmax-widthでheightを非連続に変えていくことにしました。
またブラウザ縮小時に画像上下に白い余白ができるのを避けるため、幅1000px以下では背景画像サイズを150%拡大しています。
このあたりの数値は挙動を見ながら勘で設定しています。
正確に計算すれば、もっとスムーズにレスポンシブ対応できるかもしれません。
苦肉の策でベストなやり方ではないと思いますが、これでパノラマ画像のフルスクリーン表示後も、元のレイアウトに復帰できるようになりました。
高さがずれる問題
またPanolensでパノラマ画像を読み込むと、その下のpタグの高さがなぜか28px伸びて下にずれるという怪現象が生じました。
Panolensによってcanvasやdiv要素が追加されるせいか、原因はよくわかりません。
対策としてpタグの上部marginを60pxくらい大きめに設定しておくと、パノラマ画像読み込み後の影響を相殺してレイアウトが崩れなくなります。
しかし画面上に余白が空きすぎて間抜けに見える懸念もあり、今回はデメリットを承知でそのまま残すことにしました。
そのためPCでもスマホでも、プレビュー画像をクリックすると、その下のテキストが少し下に動いてしまう問題は残っています。
initialLookAtについて
最後にスクリプトの書き方について、つまづいた部分をメモしておきます。
Panolensのドキュメントによると、Viewerで視点の初期方向をあらわすinitialLookAtを設定できるようです。
これを使えば、パノラマ画像のなかの一番見せたい部分を、初期位置にもってくることができるはずです。
ところがここにどんな値を入れても、視線は変化しませんでした。
Panolensの現行バージョンでは、initialLookAtが無効になっている可能性があります。
RawGitでは有効
自分は最初、こちらのサンプルコードを参考にして、RawGitというCDNからPanolensを利用していました。
<script src=”https://rawgit.com/pchen66/panolens.js/dev/build/panolens.min.js”></script>
RawGitは2018年時点でサービス修了を宣言していますが、2024年現在もまだ稼働しているようです。
この方法でPanolensを読み込むと、なぜかinitialLookAtも有効になっていて、デフォルト視線方向を設定できます。
具体的なやり方を以下にご説明します。
カメラを水平に設定する方法
ドキュメントにおけるViewerオプションinitialLookAtの初期値は、以下のように設定されています。
new THREE.Vector3( 0, 0, -Number.MAX_SAFE_INTEGER )
-Z方向を向かせるなら(0, 0, -1)の単位ベクトルでもよいかと考えたのですが、なぜか視線は下のほうを向いてしまいます。
ちなみに(0, 0, 0)だと完全に真下の地面を向きます。
(0, 0, -2)、(0, 0, -3)、(0, 0, -10)…と徐々にZ座標の絶対値を増やしていったところ、視線が真下から水平方向に変わっていくことに気づきました。
おそらくこのパラメータは内部的に、Three.jsのlookAt()メソッドに対応していると思われます。
またY-upのWebGL座標系において、Panolensのカメラ位置は原点よりやや上側(Y軸正方向)に設定されていると推測できます。
そのためX~Z軸の無限遠をlootAtさせることで、カメラを水平方向に向かせることができるというわけです。
ドキュメントでNumber.MAX_SAFE_INTEGERという見慣れない表現が使われている理由がわかりました。
パノラマ画像の中央を向かせたい
ちなみにX軸のマイナス方向で無限遠を設定すると、1:2横長パノラマ画像の中央を指定することができました。
initialLookAt: new THREE.Vector3( Number.MAX_SAFE_INTEGER , 0, 0)
たいていのパノラマ画像では、このように画像の中心に見栄えのする部分を配置していると思います。
RawGitのCDNを参照して上記のように設定すれば、画像中央をデフォルトの視点位置に設定できて便利です。
Panolensバージョン0.11.0の場合
冒頭に掲載したサンプルコードでは、jsDelivrのCDNからPanolensのバージョン0.11.0を参照しています。
この場合はViewerのinitialLookAtに値を入れても反応せず、パノラマ画像の中央よりやや右側が初期位置になります。
今回は仕方なくその位置にあわせてスクリーンショットを撮り、パノラマ表示前のプレビュー画像を作成しました。
サムネールと実際の画像がずれていても、それほど気にならないかもしれません。
なんとかスムーズな画面遷移を実現したくて、個人的にこだわってみたポイントです。