8.3 glTFモデルのアニメーション
動的にglTFモデルの位置・向きを変更するアニメーションを作成する方法を説明します。
1. サンプルコード
動的にglTFモデルの位置・向きを変更するアニメーションを作成する glTFModelAnimation.html 及び、 glTFModelAnimation.js のサンプルコードとシーンファイル( glTFAnimation.json )です。 このサンプルコードでは、glTFモデルが京都御所沿いの道路を北上したのち、西向きに向きを変え、さらに西進するアニメーションを表現します。
1.1. glTFデータの入手
Sketchfabへアクセスし、glTFファイルフォーマットのデータをダウンロードする、 もしくはダウンロードリンクをクリックしてダウンロードしてください。 ダウンロードリンクからダウンロードした場合はzipファイルを展開してご利用ください。 展開したデータは解凍した結果できたディレクトリを含めて、mapray-jsのルートディレクトリからの相対パスで以下のディレクトリに保存されているという想定で以下の説明を行います。
./examples/entity/gltf/data/
なお、データは当社の著作物ではありません。著作権は各データの作成者に帰属します。詳細はフォルダ中のLICENSEファイルを参照の上ご利用ください。 ユーザーの皆様がコンテンツの権利を侵害した場合、当社では一切責任を追うものではありませんのでご注意ください。
1.2. glTFModelAnimation.html
1: <!DOCTYPE html> 2: <html> 3: <head> 4: <meta charset="utf-8"> 5: <title>glTFModelAnimationSample</title> 6: <script src="https://resource.mapray.com/mapray-js/v0.9.5/mapray.min.js"></script> 7: <link rel="stylesheet" href="https://resource.mapray.com/styles/v1/mapray.css"> 8: <script src="glTFModelAnimation.js"></script> 9: <style> 10: html, body { 11: height: 100%; 12: margin: 0; 13: } 14: 15: div#mapray-container { 16: display: flex; 17: position: relative; 18: height: 100%; 19: } 20: </style> 21: </head> 22: 23: <body onload="new ModelAnimation('mapray-container');"> 24: <div id="mapray-container"></div> 25: </body> 26: </html>
1.3. glTFModelAnimation.js
1: // JavaScript source code 2: class ModelAnimation extends mapray.RenderCallback { 3: constructor(container) { 4: super(); 5: 6: // Access Tokenを設定 7: var accessToken = "<your access token here>"; 8: 9: // Viewerを作成する 10: new mapray.Viewer(container, { 11: render_callback: this, 12: image_provider: this.createImageProvider(), 13: dem_provider: new mapray.CloudDemProvider(accessToken) 14: }); 15: 16: // glTFモデルのライセンス表示 17: this.viewer.attribution_controller.addAttribution( { 18: display: "Created by modifying truck-wip by Renafox: Creative Commons - Attribution", 19: link: "https://sketchfab.com/3d-models/truck-wip-33e925207e134652bd8c2465e5c16957" 20: } ); 21: 22: this.animation_Path = [{ longitude: 135.759309, latitude: 35.024954, height: 55.0 }, // モデルを移動させるパス。場所は鳥丸通の鳥丸下長者町交差点付近 23: { longitude: 135.759309, latitude: 35.026257, height: 55.0 }, // 場所は鳥丸通と一条通の交差点付近 24: { longitude: 135.759309, latitude: 35.026257, height: 55.0 }, // 場所は鳥丸通と一条通の交差点付近 25: { longitude: 135.757438, latitude: 35.026257, height: 55.0 }]; // 場所は一条通の京都市立上京中学校前付近 26: this.model_Point = new mapray.GeoPoint(this.animation_Path[0].longitude, this.animation_Path[0].latitude, this.animation_Path[0].height); // モデルの球面座標(経度、緯度、高度) 27: this.model_Angle = 0; // モデルの向いている向き 28: this.isLoadedModel = false; // モデルをロードできたか 29: this.move_Correction = 0.00007; // 移動量の補正値 30: this.ratio_Increment = 0.15; // 毎フレームの線形補間割合増加分 31: this.ratio = 0.0; // 線形補間の割合 32: this.angle_Animation_Interval = [0, 0, 90, 90]; // 角度アニメーションの変化量データ 33: this.animation_Index = 0; 34: 35: this.SetCamera(); 36: 37: this.LoadScene(); 38: } 39: 40: // override 41: onStart() 42: { 43: // 初期の割合 44: this.ratio = 0.0; 45: } 46: 47: // override フレーム毎に呼ばれるメソッド 48: onUpdateFrame(delta_time) 49: { 50: if (this.isLoadedModel == false) { 51: return; 52: } 53: 54: // 次の線形補間の割合 55: this.ratio += this.ratio_Increment * delta_time; 56: 57: if (this.ratio > 1.0) { 58: this.ratio = 0.0; 59: this.animation_Index += 1; 60: } 61: 62: if (this.animation_Index == this.animation_Path.length - 1) { 63: this.animation_Index = 0 64: } 65: 66: this.model_Point.longitude = this.animation_Path[this.animation_Index].longitude * (1 - this.ratio) + this.animation_Path[this.animation_Index + 1].longitude * this.ratio; 67: this.model_Point.latitude = this.animation_Path[this.animation_Index].latitude * (1 - this.ratio) + this.animation_Path[this.animation_Index + 1].latitude * this.ratio; 68: this.model_Point.height = this.animation_Path[this.animation_Index].height * (1 - this.ratio) + this.animation_Path[this.animation_Index + 1].height * this.ratio; 69: 70: this.model_Angle = this.angle_Animation_Interval[this.animation_Index] * (1 - this.ratio) + this.angle_Animation_Interval[this.animation_Index + 1] * this.ratio; 71: 72: this.UpdateModelPosition(); 73: } 74: 75: // 画像プロバイダを生成 76: createImageProvider() { 77: return new mapray.StandardImageProvider( { url: "https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/", format: "jpg", min_level: 2, max_level: 18 } ); 78: } 79: 80: // カメラ位置の設定 81: SetCamera() { 82: // 球面座標系(経度、緯度、高度)で視点を設定。座標は京都御所前 83: var home_pos = { longitude: 135.759366, latitude: 35.025891, height: 50.0 }; 84: 85: // 球面座標から地心直交座標へ変換 86: var home_view_geoPoint = new mapray.GeoPoint( home_pos.longitude, home_pos.latitude, home_pos.height ); 87: var home_view_to_gocs = home_view_geoPoint.getMlocsToGocsMatrix( mapray.GeoMath.createMatrix() ); 88: 89: // 視線方向を定義 90: var cam_pos = mapray.GeoMath.createVector3([-400, 10, 400]); 91: var cam_end_pos = mapray.GeoMath.createVector3([0, 0, 0]); 92: var cam_up = mapray.GeoMath.createVector3([0, 0, 1]); 93: 94: // ビュー変換行列を作成 95: var view_to_home = mapray.GeoMath.createMatrix(); 96: mapray.GeoMath.lookat_matrix(cam_pos, cam_end_pos, cam_up, view_to_home); 97: 98: // カメラの位置と視線方向からカメラの姿勢を変更 99: var view_to_gocs = this.viewer.camera.view_to_gocs; 100: mapray.GeoMath.mul_AA(home_view_to_gocs, view_to_home, view_to_gocs); 101: 102: // カメラのnear farの設定 103: this.viewer.camera.near = 30; 104: this.viewer.camera.far = 500000; 105: } 106: 107: // シーンの読み込み 108: LoadScene() { 109: var scene_File_URL = "http://localhost/glTF/glTFAnimation.json"; 110: 111: // シーンを読み込む 112: var loader = new mapray.SceneLoader(this.viewer.scene, scene_File_URL, { 113: transform: (url, type) => this.onTransform(url, type), 114: callback: (loader, isSuccess) => { 115: this.onLoadScene(loader, isSuccess); 116: } 117: }); 118: 119: loader.load(); 120: } 121: 122: onTransform(url, type) { 123: return { 124: url: url, 125: credentials: mapray.CredentialMode.SAME_ORIGIN, 126: headers: {} 127: }; 128: } 129: 130: onLoadScene(loader, isSuccess) { 131: if (isSuccess) { 132: this.isLoadedModel = true; 133: 134: this.UpdateModelPosition(); 135: } 136: } 137: 138: UpdateModelPosition() { 139: // sceneのEntityを取得 140: var entity = this.viewer.scene.getEntity(0); 141: 142: // モデルの位置を設定 143: entity.setPosition(this.model_Point); 144: 145: // モデルの回転 146: entity.setOrientation(new mapray.Orientation(-this.model_Angle, 0, 0)); 147: } 148: 149: }
1.4. シーンファイル(glTFAnimation.json)
1: { 2: "model_register": { 3: "model-0": { 4: "link": "./truck_wip/scene.gltf", 5: "offset_transform": { "tilt": -90, "scale": 0.1 } 6: } 7: }, 8: "entity_list": [{ 9: "type": "model", 10: "mode": "basic", 11: "transform": { "position": [135.759366, 35.025891, 55.0] }, 12: "ref_model": "model-0", 13: "altitude_mode": "absolute" 14: } 15: ] 16: }
2. htmlのサンプルコードの詳細
htmlのサンプルコードの詳細を以下で解説します。
2.1. htmlの文字コード設定
4行目でhtmlの文字コードを設定します。このサンプルコードでは、utf-8を設定します。
4: <meta charset="utf-8">
2.2. タイトルの設定
5行目でタイトルを設定します。このサンプルコードでは、glTFModelAnimationSampleを設定します。
5: <title>glTFModelAnimationSample</title>
2.3. JavaScriptファイルのパス設定
6~8行目で参照するJavaScript及びスタイルシートのパスを設定します。このサンプルコードでは、maprayのJavaScriptファイル、スタイルシート、モデルのアニメーションJavaScriptファイル( glTFModelAnimation.js )を設定します。
6: <script src="https://resource.mapray.com/mapray-js/v0.9.5/mapray.min.js"></script> 7: <link rel="stylesheet" href="https://resource.mapray.com/styles/v1/mapray.css"> 8: <script src="glTFModelAnimation.js"></script>
2.4. スタイルの設定
9~20行目で表示する要素のスタイルを設定します。スタイルの詳細は、ヘルプページ『緯度経度によるカメラ位置の指定』を参照してください。
9: <style> 10: html, body { 11: height: 100%; 12: margin: 0; 13: } 14: 15: div#mapray-container { 16: display: flex; 17: position: relative; 18: height: 100%; 19: } 20: </style>
2.5. loadイベントの処理
画面を表示するときに、glTFモデルアニメーションクラスを作成します。そのため、23行目でページの読み込み時に、地図表示部分のブロックのidからglTFモデルアニメーションクラスのインスタンスを生成します。 glTFモデルアニメーションクラスはJavaScriptのサンプルコードの詳細で説明します。
23: <body onload="new ModelAnimation('mapray-container');">
2.6. 地図表示部分の指定
24行目で地図表示部分のブロックを記述します。 詳細はヘルプページ『緯度経度によるカメラ位置の指定』を参照してください。
24: <div id="mapray-container"></div>
3. JavaScriptのサンプルコードの詳細
JavaScriptのサンプルコードの詳細を以下で解説します。
3.1. クラスとグローバル変数の説明
1~148行目のクラスは、glTFモデルアニメーションクラスです。アニメーションを表現するために、glTFモデルアニメーション作成クラスは、mapray.RenderCallbackクラスを継承します。
class ModelAnimation extends mapray.RenderCallback { //中略 }
3.2. コンストラクタ
2~37行目がモデルのアニメーションクラスのコンストラクタです。 まず、引数として渡されるブロックのidに対して、mapray.Viewerを作成し、glTFモデルの出典情報を追加します。mapray.Viewerのベース地図の画像プロバイダは、画像プロバイダの生成メソッドで取得した画像プロバイダを設定します。mapray.Viewerの作成の詳細は、ヘルプページ『カメラのアニメーション』を参照してください。 次に、glTFモデルの操作に関する初期値を下記のように設定します。
- 移動時の経由点の緯度、経度、高度 ⇒ 開始位置、方向転換開始位置、方向転換終了位置、終了位置
- 現在の位置の緯度、経度、高度 ⇒ 開始位置 - 現在の向き ⇒ 0度
- ロードの成功可否 ⇒ false
- 移動量 ⇒ 0.00007度
- glTFモデル位置の線形補間時の1秒当たりの増加割合 ⇒ 0.15
- glTFモデル位置の線形補間時の現在の割合 ⇒ 0
- 経由点でのglTFモデルの向き ⇒ 0、0、90、90
- glTFモデル位置の線形補間対象となる区間番号 ⇒ 0
最後に、カメラの位置・向きの設定、シーンのロードの順にメソッドを呼び出します。
2: constructor(container) { 3: super(); 4: 5: // Access Tokenを設定 6: var accessToken = "<your access token here>"; 7: 8: // Viewerを作成する 9: new mapray.Viewer(container, { 10: render_callback: this, 11: image_provider: this.createImageProvider(), 12: dem_provider: new mapray.CloudDemProvider(accessToken) 13: }); 14: 15: // glTFモデルのライセンス表示 16: this.viewer.attribution_controller.addAttribution( { 17: display: "Created by modifying truck-wip by Renafox: Creative Commons - Attribution", 18: link: "https://sketchfab.com/3d-models/truck-wip-33e925207e134652bd8c2465e5c16957" 19: } ); 20: 21: this.animation_Path = [{ longitude: 135.759309, latitude: 35.024954, height: 55.0 }, // モデルを移動させるパス。場所は鳥丸通の鳥丸下長者町交差点付近 22: { longitude: 135.759309, latitude: 35.026257, height: 55.0 }, // 場所は鳥丸通と一条通の交差点付近 23: { longitude: 135.759309, latitude: 35.026257, height: 55.0 }, // 場所は鳥丸通と一条通の交差点付近 24: { longitude: 135.757438, latitude: 35.026257, height: 55.0 }]; // 場所は一条通の京都市立上京中学校前付近 25: this.model_Point = new mapray.GeoPoint(this.animation_Path[0].longitude, this.animation_Path[0].latitude, this.animation_Path[0].height); // モデルの球面座標(経度、緯度、高度) 26: this.model_Angle = 0; // モデルの向いている向き 27: this.isLoadedModel = false; // モデルをロードできたか 28: this.move_Correction = 0.00007; // 移動量の補正値 29: this.ratio_Increment = 0.15; // 毎フレームの線形補間割合増加分 30: this.ratio = 0.0; // 線形補間の割合 31: this.angle_Animation_Interval = [0, 0, 90, 90]; // 角度アニメーションの変化量データ 32: this.animation_Index = 0; 33: 34: this.SetCamera(); 35: 36: this.LoadScene(); 37: }
3.3. レンダリングループの開始時のコールバックメソッド
40~44行目がレンダリングループの開始時のコールバックメソッドです。 レンダリングループの開始時のコールバックメソッドの詳細は、ヘルプページ『パスに沿ったカメラアニメーション』を参照してください。
39: // override 40: onStart() 41: { 42: // 初期の割合 43: this.ratio = 0.0; 44: }
3.4. フレームレンダリング前のコールバックメソッド(glTFモデルの位置・向きの後進処理)
47~72行目がフレームレンダリング前のコールバックメソッドです。このサンプルコードでは、この中で、glTFモデルが正常に読み込まれている場合は、glTFモデルの位置・向きの更新処理を行います。 まず、56~63行目で、引数の経過時間をもとに、線形補間時の現在の割合を計算します。その際、現在の割合が1より大きくなった場合は、線形補間対象となる区間番号を1つ増やし、現在の割合を0に設定します。また、全ての区間を補間し終えた場合は、区間番号0にリセットします。 次に、65~67行目で、線形補間の対象区間の緯度・経度・高度を線形補間し、現在の位置となる緯度・経度・高度を求めます。また、69行目で、線形補間の対象区間のglTFモデルの向きを線形補間し、現在のglTFモデルの向きを求めます。 最後に、glTFモデルの姿勢変換行列の設定メソッドを呼び出し、現在の位置、向きを用いて、glTFモデルの姿勢変換行列を現在の状態に更新します。なお、glTFモデルの姿勢変換行列の詳細は後述します。
46: // override フレーム毎に呼ばれるメソッド 47: onUpdateFrame(delta_time) 48: { 49: if (this.isLoadedModel == false) { 50: return; 51: } 52: 53: // 次の線形補間の割合 54: this.ratio += this.ratio_Increment * delta_time; 55: 56: if (this.ratio > 1.0) { 57: this.ratio = 0.0; 58: this.animation_Index += 1; 59: } 60: 61: if (this.animation_Index == this.animation_Path.length - 1) { 62: this.animation_Index = 0 63: } 64: 65: this.model_Point.longitude = this.animation_Path[this.animation_Index].longitude * (1 - this.ratio) + this.animation_Path[this.animation_Index + 1].longitude * this.ratio; 66: this.model_Point.latitude = this.animation_Path[this.animation_Index].latitude * (1 - this.ratio) + this.animation_Path[this.animation_Index + 1].latitude * this.ratio; 67: this.model_Point.height = this.animation_Path[this.animation_Index].height * (1 - this.ratio) + this.animation_Path[this.animation_Index + 1].height * this.ratio; 68: 69: this.model_Angle = this.angle_Animation_Interval[this.animation_Index] * (1 - this.ratio) + this.angle_Animation_Interval[this.animation_Index + 1] * this.ratio; 70: 71: this.UpdateModelPosition(); 72: }
3.5. 画像プロバイダの生成
75~77行目が画像プロバイダの生成メソッドです。生成した画像プロバイダを返します。 画像プロバイダの生成の詳細は、ヘルプページ『緯度経度によるカメラ位置の指定』を参照してください。
74: // 画像プロバイダを生成 75: createImageProvider() { 76: return new mapray.StandardImageProvider( { url: "https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/", format: "jpg", min_level: 2, max_level: 18 } ); 77: }
3.6. カメラの位置・向きの設定
80~104行目がカメラの位置・向きの設定メソッドです。 カメラの位置・向きの設定は、ヘルプページ『緯度経度によるカメラ位置の指定』を参照してください。
79: // カメラ位置の設定 80: SetCamera() { 81: // 球面座標系(経度、緯度、高度)で視点を設定。座標は京都御所前 82: var home_pos = { longitude: 135.759366, latitude: 35.025891, height: 50.0 }; 83: 84: // 球面座標から地心直交座標へ変換 85: var home_view_geoPoint = new mapray.GeoPoint( home_pos.longitude, home_pos.latitude, home_pos.height ); 86: var home_view_to_gocs = home_view_geoPoint.getMlocsToGocsMatrix( mapray.GeoMath.createMatrix() ); 87: 88: // 視線方向を定義 89: var cam_pos = mapray.GeoMath.createVector3([-400, 10, 400]); 90: var cam_end_pos = mapray.GeoMath.createVector3([0, 0, 0]); 91: var cam_up = mapray.GeoMath.createVector3([0, 0, 1]); 92: 93: // ビュー変換行列を作成 94: var view_to_home = mapray.GeoMath.createMatrix(); 95: mapray.GeoMath.lookat_matrix(cam_pos, cam_end_pos, cam_up, view_to_home); 96: 97: // カメラの位置と視線方向からカメラの姿勢を変更 98: var view_to_gocs = this.viewer.camera.view_to_gocs; 99: mapray.GeoMath.mul_AA(home_view_to_gocs, view_to_home, view_to_gocs); 100: 101: // カメラのnear、farの設定 102: this.viewer.camera.near = 30; 103: this.viewer.camera.far = 500000; 104: }
3.7. シーンのロード
107~119行目がシーンのロードメソッドです。 シーンのロードは、ヘルプページ『glTFモデルの表示(SceneLoaderを使った表示)』を参照してください。
106: // シーンの読み込み 107: LoadScene() { 108: var scene_File_URL = "./data/glTFAnimation.json"; 109: 110: // シーンを読み込む 111: var loader = new mapray.SceneLoader(this.viewer.scene, scene_File_URL, { 112: transform: (url, type) => this.onTransform(url, type), 113: callback: (loader, isSuccess) => { 114: this.onLoadScene(loader, isSuccess); 115: } 116: }); 117: 118: loader.load(); 119: }
3.8. リソース要求変換
121~127行目がリソース要求変換メソッドです。 リソース要求変換は、ヘルプページ『glTFモデルの表示(SceneLoaderを使った表示)』を参照してください。
121: onTransform(url, type) { 122: return { 123: url: url, 124: credentials: mapray.CredentialMode.SAME_ORIGIN, 125: headers: {} 126: }; 127: }
3.9. シーンのロード終了イベント
129~135行目がシーンのロード終了イベントメソッドです。引数のisSuccessには、読み込み結果が格納されており、trueの場合のみ読み込んだglTFモデルを表示し、glTFモデルを操作できるようにします。 glTFモデルのロード成功可否をtrueにし、glTFモデルの表示位置を設定するメソッドを呼び出します。glTFモデルの表示位置を設定するメソッドの詳細は後述します。
129: onLoadScene(loader, isSuccess) { 130: if (isSuccess) { 131: this.isLoadedModel = true; 132: 133: this.UpdateModelPosition(); 134: } 135: }
3.10. glTFモデルの表示位置の設定
137~146行目がglTFモデルの表示位置の設定メソッドです。モデルの表示位置、向きをモデルのエンティティに反映します。 142行目でモデルの表示位置を、144行目でモデルの向きをそれぞれ設定します。 なお、読み込んだモデルは1つ目のエンティティとなるため、エンティティ取得時の引数には0を指定します。
137: UpdateModelPosition() { 138: // sceneのEntityを取得 139: var entity = this.viewer.scene.getEntity(0); 140: 141: // モデルの位置を設定 142: entity.setPosition(this.model_Point); 143: 144: // モデルの回転 145: entity.setOrientation(new mapray.Orientation(-this.model_Angle, 0, 0)); 146: }
4. シーンファイルの詳細
シーンファイルの詳細を以下で解説します。なお、シーンファイルはJSON形式で記述します。
4.1. エンティティの設定
8行目でentity_listという名称でエンティティを定義し、その中にエンティティの詳細を定義します。9行目のtypeという名称は、エンティティの種類を表し、glTFモデルの場合はmodelを指定します。
{ 中略 "entity_list": [{ "type": "model", 中略 } ] }
4.2. glTFモデルのデータ
2~7行目でmodel_registerという名称でモデルデータを定義します。このシーンファイルでは、モデルデータのIDをmodel-0とし、モデルファイルをファイルから読み込むために、linkという名称にglTFファイルのURLを指定します。 また、モデルの初期向きとスケールをoffset_transformという名称で指定することができます。今回のデータでは下記の内容を定義します。
- チルト(Y軸回りの回転角度)(tilt) ⇒ -90
- モデルスケール(scale) ⇒ 0.1
2: "model_register": { 3: "model-0": { 4: "link": "./truck_wip/scene.gltf", 5: "offset_transform": { "tilt": -90, "scale": 0.1 } 6: } 7: },
4.3. 汎用エンティティの設定
8~12行目で汎用エンティティの設定をします。汎用エンティティには以下の内容を定義します。
- モード(mode) ⇒ basic
- 初期姿勢(transform) ⇒ 球面座標系(position)での初期位置
- モデルデータ(ref_model) ⇒ モデルデータのID(model-0)
- 高度モード(altitude_mode) ⇒ 初期位置の高度を絶対値で指定(absolute)
8: "type": "model", 9: "mode": "basic", 10: "transform": { "position": [135.759366, 35.025891, 55.0] }, 11: "ref_model": "model-0", 12: "altitude_mode": "absolute"