Yandex.Panorams API:仮想歩行を行う方法、または地下鉄から人を連れて行く方法

サイトにYandex Panoramasを埋め込むことができるAPIを作成するために非常に長い時間を求められ、最終的にこれを行うことができました。 さらに:APIを使用すると、独自のパノラマを作成できます。







この投稿では、このような仮想ウォークを行うために一般的に知っておく必要があることを説明します。 APIを簡単に作成できなかった理由。さまざまな問題を解決し、APIでできることを一目でわかるように説明しました。













エンジン



パノラマサービスは、2009年9月にYandex.Mapsで開始されました。 最初は、これらはほんの数景のパノラマであり、おそらくご想像のとおり、Flashで機能していました。 それ以来、大量の水が流れ、パノラマが数百万になり、モバイルプラットフォームが急速に成長し始め、Flashはそこに到達しませんでした。 したがって、2013年頃、新しいテクノロジーが必要であると判断しました。 そして、このテクノロジーの基礎はHTML5でした。







始めたAPIはCanvas2Dです。 これは奇妙に思えるかもしれませんが、2013年にはこの選択は非常に合理的でした。 WebGLは2年前に標準化されましたが、実際には携帯電話には届きませんでした(たとえば、iOSでは、WebGLはiAdでしか動作しませんでしたが、ほぼ完全に死亡しました)。 読者は、当時人気があったため、すべてをCSS 3Dで行う必要があることに反対することができます。 ただし、CSS 3Dを使用すると、立方体のパノラマのみを描画できますが、すべてのYandexパノラマは球形です(等間隔の投影で保存されます)。







これは、開発における最も重要な技術的困難でもありました。 事実、この変換の非線形性のために、球面パノラマをスクリーンに正確かつ正確に投影することは容易ではありません。 このような投影の単純な実装では、画面の各ピクセルの三角法計算のヒープ全体が必要になります。結局、パノラマ画像内の対応する点を見つけてその色を決定する必要があります。 さらに、Canvas 2Dは、画像の各ピクセルを個別に操作する効果的な方法を提供しません。







この困難な状況を解決するために、コンピューターグラフィックスで最も古いトリックの1つである線形補間が使用されます。 画面の各ピクセルについて、パノラマ画像の対応する点の座標を正確に計算しません。 これは、事前に選択した一部のピクセルに対してのみ行います。残りのピクセルについては、既に計算されたピクセル間を補間することで座標を見つけます。 唯一の問題は、これらのピクセルを選択する方法です。







既に述べたように、Canvas2Dでピクセル単位で画像の色を記録するのは不便ですが、画像とその2次元変換を使用すると便利です。 さらに、このような変換は実際に補間を実装します。 パノラマレンダリングアルゴリズムの基礎として使用することに決めたのは、彼らでした。 また、2次元線形変換はスクリーン上の1つとパノラマ画像上の2つのトリプレットポイントによって一意に決定されるため、投影を正確に考慮するポイントセットの選択はそれ自体で得られます。パノラマ球体を三角形に分割します。 最終的なレンダリングアルゴリズムは次のとおりです。







Canvas2Dレングスダングリングオルゴールズム







これをすべて書いて実行した後、「スライドショー」という言葉でよく説明されているものを見ました。 フレームレートは完全に受け入れられませんでした。 プロファイリングでは、Canvas 2Dコンテキストのsave()



およびrestore()



機能が最も多く消費されることがわかりました。 彼らはどこから来たのですか? Canvas2Dのトリミング機能。 残念ながら、現在のクロップをリセットして新しいクロップを設定できるようにするには、設定する前にコンテキストの状態を保存する必要があり(これは単にsave()



)、必要なすべての描画の後、保存された状態を復元します(そして、これはrestore()



) これらの操作は州全体で機能するため

コンテキスト、彼らは安くはありません。 さらに、毎回同じトリミングを行います(初期化後、球体の三角形への分割とパノラマ画像への配置は変わりません)。 キャッシュするのは理にかなっています!







すぐに言ってやった。 三角形の球体を生成した後、パノラマ画像から各三角形を「切り取り」、個別のキャッシュキャンバスに保存します。 このキャッシュの残りは透明のままです。 この最適化の後、モバイルデバイスでも30〜60フレーム/秒を取得できました。 この経験から、次のレッスンを学ぶことができます。Canvas2Dでレンダリングを開発するとき、キャッシュおよびプリレンダリングできるすべてのもの。 そして、何かが突然不可能な場合-可能な方法でそれを行い、また再レンダリングします。







三角形キャシシュのスライス







キャッシング(この世の多くのものと同様)には欠点があります:メモリー消費は必然的に増加します。 これがまさにパノラマレンダリングで発生したことです。 食欲の増加は多くの問題を引き起こしています。 最も注目に値するのは、デスクトッププラットフォーム上であっても、ブラウザのクラッシュと、かなり遅い起動です。 最後に、これらの問題に対処するのにうんざりして、Canvas 2Dでパノラマ画像を再プログラムすることを拒否し、その逆を行いました。 しかし、彼は絶対に面白くない:)







ただし、その前でさえ、WebGLの側面から見始めました。 このため、さまざまな理由に促されましたが、その主な理由はおそらく、iOS 8で、WebGLはSafariで機能していました。







レンダリングのWebGLバージョンを開発する際の主な問題は、パノラマのサイズでした。 パノラマ画像はどのテクスチャにも適合しませんでした。 「世界の古い原則に従う」という原則に導かれ、この問題を再び解決し、パノラマ球体をセクターに分割しました。 各セクターには独自のテクスチャがあります。 同時に、メモリとGPUリソ​​ースを節約するために、非表示のセクターは完全に削除されます。 それらが再び画面に表示される必要がある場合、それらのデータは再び(通常はブラウザキャッシュから)リロードされます。







パノラマを埋め込む



Maps APIを使用してパノラマを埋め込むには、まず必要なモジュールを接続します。 これは2つの方法で行うことができます:APIを接続するときにload



パラメーターで指定するか、モジュラーシステムを使用します(ロードされるモジュールのデフォルトセットにパノラマモジュールを追加します)。







 <!--       API. --> <script src="https://api-maps.yandex.ru/2.1/?lang=ru_RU&amp;load=panorama.locate,panorama.Player"></script> <script> //     . ymaps.modules.require([ 'panorama.createPlayer', 'panorama.isSupported' ]) .done(function () { // ... }); </script>
      
      





パノラマの操作を開始する前に、ユーザーのブラウザがエンジンでサポートされていることを確認する必要があります。 これは、 ymaps.panorama.isSupported



関数を使用してymaps.panorama.isSupported



ます。







 if (!ymaps.panorama.isSupported()) { //   ,   // , etc. } else { //   . }
      
      





パノラマを開くには、まずサーバーから説明を取得する必要があります。 これは、 ymaps.panorama.locate



関数を使用して行われます。







 ymaps.panorama.locate( [0, 0] //   ).done(function (panoramas) { // ... });
      
      





ymaps.panorama.locate



によって返されたプロミスを解決する結果は、送信されたポイントの近くにあるパノラマの配列になります。 そのようなパノラマがない場合、配列は空になります。 そのようなパノラマが複数ある場合、送信されたポイントからの距離でソートされます。 最初のものが最も近くなります。







引き続き空中パノラマをリクエストできます:







 ymaps.panorama.locate( [0, 0], //   { layer: 'yandex#airPanorama' } ).done(function (panoramas) { // ... });
      
      





パノラマの説明を受け取ったら、ページに表示するプレーヤーを作成できます。







 var player = new ymaps.panorama.Player( 'player', // ID ,      panorama // ,       );
      
      





ページに表示されます:







パノプレーヤー







パノラマを開く最も速くて簡単な方法は、 ymaps.panorama.createPlayer



関数です。







 ymaps.panorama.createPlayer( 'player', // ID DOM-,      [0, 0] //  ,     ).done(function (player) { // ... });
      
      





この場合、3番目のパラメーター 1つ以上のオプションを指定できます。







 ymaps.panorama.createPlayer( 'player', [0, 0], { // ,     layer: 'yandex#airPanorama', //   direction: [120, 10], //      span: [90, 90], //   controls: ['zoomControl', 'closeControl'] } ).done(function (player) { // ... });
      
      





プレーヤーは、作成されると、パノラマの表示を制御し、ユーザーイベントをサブスクライブするためのコンパクトなAPIを提供します。 しかし、私には思えますが、これはパノラマAPIの最も興味深い機能ではありません。







自分のパノラマ



おそらく、APIが提供する最も興味深い機能は、独自のパノラマを作成し、サイトに埋め込むことです。







パノラマはすべて、パノラマ画像の撮影と準備から始まります。 撮影には、特別なデバイス、通常のカメラ、またはスマートフォンを使用できます。 主なものは、射撃と接着の結果が等間隔の投影での球面パノラマであることです。 たとえば、Androidの標準カメラアプリケーションでは、目的の投影でパノラマを撮影して接着できます。 これを使用して、居心地の良い広場のパノラマを撮影しました。







パノラマ画像を撮影した後、プレーヤーで表示するための準備が必要です。 これを行うには、タイルにカットする必要があります。 さらに、さまざまなレベルのマスターベーション用に、いくつかの異なるサイズの画像を準備できます。 プレーヤーは、プレーヤーが開いているDOM要素の現在のサイズと、パノラマの表示領域の角度寸法に最適なレベルを選択します。 また、画像の最小レベルが2048ピクセル未満の場合、「プログレッシブジープエフェクト」の作成に使用されます。 プレーヤーは、このレベルのタイルを最も高い優先度でロードし、より良いタイルがない場所を表示します(たとえば、まだロードされていないか、メモリから消去されてまだリブートされていない場合)。







任意のソフトウェアを使用して、画像をタイルにカットできます(特定の忍耐力がある場合-少なくともペイント)。 タイルのサイズは2のべき乗である必要があります(WebGLで作業したことがある人は、この手足の位置がどこから来たのか推測します)。 ImageMagickを利用しました。







 #      ,    #       (  , ,  ). $ convert src.jpg -resize 7168x3584 hq.jpg #      512  512 . $ convert hq.jpg -crop 512x512 \ -set filename:tile "%[fx:page.x/512]-%[fx:page.y/512]" \ "hq/%[filename:tile].jpg" #       « #  ».      ,   #    . $ convert hq.jpg -resize 512x256 lq/0-0.jpg
      
      





最後に、パノラマ用のある種のコードを書きましょう。 APIは、相互接続されたインターフェイスのシステムです。 これらのインターフェイスは、パノラマオブジェクトとそれに関連付けられたすべてのオブジェクトを記述します。







インターフフーース







この図をエンティティごとに分析してみましょう。







パノラマオブジェクトはIPanorama



インターフェイスを実装する必要があります。 パノラマクラスを作成するのは簡単で、抽象クラスymaps.panorama.Base



。 いくつかのIPanorama



メソッドの妥当なデフォルト実装と、パノラマがプレーヤーによって課された制限を満たしているかどうかを確認するvalidate



メソッド(たとえば、指定されたタイルサイズが2のべき乗かどうか)を提供します。 それを使用しましょう。







 function Panorama () { Panorama.superclass.constructor.call(this); // ... //  ,     . this.validate(); } ymaps.util.defineClass(Panorama, ymaps.panorama.Base, { // ,   . });
      
      





まず、プレーヤーのパノラマジオメトリについて説明します。 これを行うには、ドキュメントで判断すると、4つの数値の配列を返すgetAngularBBox



メソッドを実装する必要があります。 これらの数字の意味は何ですか? この質問に答えるために、パノラマは球形、つまり球に重ねられていることを思い出してください。 球上のパノラマ画像の位置を説明するには、いくつかの「参照」ポイントを選択する必要があります。 通常、長方形(および球体上にないパノラマ画像、コンピューターの画像のように、まさにそれです)の場合、2つの反対側の角の座標が選択されます。 私たちの場合、このアプローチは球体上で機能し続けます。なぜなら、画像の垂直な側面は重ね合わせると子午線になり、水平は平行になります。 これは、長方形の各辺に、この辺のすべてのポイントに共通の独自の角度座標があることを意味します。 getAngularBBox



メソッドによって返される配列を構成するのは、これらの側面の座標であり、パノラマの境界を定める一種の球形の長方形を定義します(メソッドの名前です)。







プレーヤーは、パノラマのジオメトリ(したがって、パノラマ画像自体)に重要な制限を課します。パノラマ画像は、球体上で水平に閉じて、完全な円を形成する必要があります。 getAngularBBox



メソッドによって返される値の場合、これは、パノラマの右隅と左隅の境界の差が2πであることを意味します。 垂直方向の境界線については、任意に設定できます。







スマートフォンで撮影したパノラマは、水平方向だけでなく、垂直方向、つまり極から極までの完全なパノラマです。 したがって、球面上のパノラマの境界は、間隔[π/2, -π/2]



垂直(上部極から下部)および水平[0, 2π]



間隔(ここでは簡単にするために、パノラマの接合部の方向がもちろん、そうではありません)。 このコードは次のとおりです。







 getAngularBBox: function () { return [ 0.5 * Math.PI, 2 * Math.PI, -0.5 * Math.PI, 0 ]; }
      
      





また、パノラマの位置と、パノラマが定義されている座標系を返すメソッドを実装する必要があります。 プレーヤーは、このデータを使用して、シーン内のパノラマに関連付けられているオブジェクト(下のオブジェクトについて)を正しく配置します。







 getPosition: function () { //         , ... return [0, 0]; }, getCoordSystem: function () { // ...    ,     //  -   . return ymaps.coordSystem.cartesian; },
      
      





次に、パノラマ画像自体について説明します-タイルにカットされる方法と、これらのタイルが置かれる場所。 これを行うには、 getTileLevels



getTileLevels



を実装する必要がありgetTileLevels



。 最初のものでは、すべてが明らかです。タイルのサイズを返します。







 getTileSize: function () { return [512, 512]; }
      
      





getTileLevels



は、パノラマ画像のズームレベルの説明オブジェクトの配列を返します。 私はそれらの2つがあったことを思い出します:高(比較的)と低品質。 このような各説明オブジェクトは、2つのメソッドgetImageSize



およびgetTileUrl



で構成されるIPanoramaTileLevel



インターフェイスを実装する必要があります。 簡単にするために、このための個別のクラスは用意せず、必要なメソッドでオブジェクトを返すだけです。







 getTileLevels: function () { return [ { getTileUrl: function (x, y) { return '/hq/' + x + '-' + y + '.jpg'; }, getImageSize: function () { return [7168, 3584]; } }, { getTileUrl: function (x, y) { return '/lq/' + x + '-' + y + '.jpg'; }, getImageSize: function () { return [512, 256]; } } ]; }
      
      





これにより、最小限のパノラマの説明が準備でき、プレーヤーはそれを表示できるようになります。







 var player = new ymaps.panorama.Player('player', new Panorama());
      
      





ところで、ヘルパー関数ymaps.panorama.Base.createPanorama



を使用すると、このような最小限のパノラマの説明をより速く簡単に作成できます。







 var player = new ymaps.panorama.Player( 'player', ymaps.panorama.Base.createPanorama({ angularBBox: [ 0.5 * Math.PI, 2 * Math.PI, -0.5 * Math.PI, 0 ], position: [0, 0], coordSystem: ymaps.coordSystem.cartesian, name: ' -', tileSize: [512, 512], tileLevels: [ { getTileUrl: function (x, y) { return '/hq/' + x + '-' + y + '.jpg'; }, getImageSize: function () { return [7168, 3584]; } }, { getTileUrl: function (x, y) { return '/lq/' + x + '-' + y + '.jpg'; }, getImageSize: function () { return [512, 256]; } } ] }) );
      
      





実際のパノラマに加えて、プレーヤーはマーカー、トランジション、コミュニケーションの3種類のオブジェクトを表示できます。







マーカーを使用すると、パノラマ内のオブジェクトを指定できます(たとえば、Yandexパノラマ内の家番号を持つマーカー)。 マーカーオブジェクトはIPanoramaMarker



インターフェイスを実装する必要があります。 このインターフェイスには、 getIconSet



getPosition



およびgetPanorama



3つのメソッドのみが含まれています。 最後の2つの目的は、名前から明らかです。 まず、説明する必要があると思います。 実際、マーカーはインタラクティブな要素です。 ユーザーイベントに応じて状態を変更します。 これらの状態と、UIのイベントに応じてどのように変化するかは、次の図で記述できます。







マーカーの状態







たとえば、家を示すマーカー。 デフォルトの状態、カーソルのホバー状態、および展開された状態は次のとおりです。







マーカーの状態







getIconSet



メソッドがプレーヤーに戻るのは、これらの状態のアイコンです。 アイコンはサーバーからダウンロードでき(これがこのメソッドが非同期になっている理由です)、手続き的に生成されます(キャンバスを使用)。 この例では、パノラマに1つのマーカーとそのアイコンが既に読み込まれていると仮定します。







 getMarkers: function () { //     ,     // ,  . var panorama = this; return [{ properties: new ymaps.data.Manager(), getPosition: function () { return [10, 10]; }, getPanorama: function () { return panorama; }, getIconSet: function () { return ymaps.vow.resolve({ 'default': { image: defaultMarkerIcon, offset: [0, 0] }, hovered: { image: hoveredMarkerIcon, offset: [0, 0] } // ,     , //   `default`. }); } }]; }
      
      





トランジションは矢印であり、クリックするとプレーヤーは隣接するパノラマに切り替わります。 遷移を記述するオブジェクトは、 IPanoramaThorougfare



インターフェイスを実装する必要があります。







 getThoroughfares: function () { //       :) var panorama = this; return [{ properties: new ymaps.data.Manager(), getDirection: function () { //    ,    // ,     . return [Math.PI, 0]; }, getPanorama: function () { return panorama; }, getConnectedPanorama: function () { //       . // ,        // ,      ymaps.panorama.locate. return ymaps.panorama.locate(/* ... */) .then(function (panoramas) { if (panoramas.lengths == 0) { return ymaps.vow.reject(); } return panoramas[0]; }); } }]; }
      
      





接続は、マーカーとトランジションの一種です。最初の接続のように見えますが、最後の接続のように動作します。 コードでは、マーカーと同じ方法で実装されていますが、 getConnectedPanorama



メソッドが追加されています( IPanoramaConnection



参照)。







結論の代わりに



パノラマAPIは現在ベータステータスです。 埋め込み、サイトおよびアプリケーションでのテスト、 クラブVKontakteのグループFacebook、またはサポートを通じてそのことをお知らせください。 すでにシアンです:)








All Articles