こんにちは、YAMAPでバックエンドエンジニアをしている @ryutaro_mizokami です。
そして、この記事は YAMAPエンジニアアドベントカレンダー の19日目です。
ちなみに、昨日の記事はYAMAPエンジニアの福利厚生的存在である @inokappa san による Serverless Days Fukuoka 2019 の思い出 〜 愛・忘れもしません 〜 でした。カンファレンスの運営に積極的に関わっていて本当に頭が上がりません。
はじめに
YAMAPは先週、3Dマップという機能をリリースしました。
(詳しくは 3Dマップ機能をリリースしました – YAMAP info blog をご覧ください。操作感がとても気持ちよく、皆さんにぜひ使ってみて欲しいです、オススメです!)
自分はこのシステムのバックエンドを担当し、この機能が成長していく過程を間近で見ていたこともあって、次第に3Dプログラミングに興味が出てきました。
フロントエンドはTypeScript+WebGLで作られているアプリケーションですが、せっかくなので自分はRust+OpenGLで何かを作ってみようと思い立ちました。
この記事はその記録です。
(自分は3Dプログラミング初心者なので説明に間違いがあるかもしれません、お手柔らかにお願いします。)
作り始める前に
自分はRustもOpenGLもハローワールドレベルなので事前に少し勉強をしました。
Rustは3回くらい入門と挫折を繰り返した経験ありです。今回は改めて The Rust Programming Language - Rustの日本語ドキュメント を前半だけ読んで過去の記憶を呼び起こしました。
OpenGLについては [DL版] RustではじめるOpenGL - toyamaguchi - BOOTH を購入して読みました。サンプルコードもついており、内容もわかりやすく、個人的にはオススメです。
何を作っているか
GPSデータの軌跡を3Dの地形上にレンダリングするアプリケーションです。進捗は動画をご覧ください。
(富士山はとにかく動画映えするとシミジミ🏔)
## どうやって作ったか最近趣味で作っている3D軌跡マップアプリについて書きました。会社のアドベントカレンダー19日目の記事でございます。https://t.co/s8IUm5jiVQ pic.twitter.com/zQTphPloLy
— かみさん (@ryutaro_mizokam) December 18, 2019
RustからOpenGLを扱うために gl を、ウィンドウを作るために sdl2 を使っています。
今回、OpenGLの世界にレンダリングするノードは、
- 地形
- GPSの軌跡(動画の黄色い線)
の2つです。それぞれ以下のように実装しました。
1. 地形
地形画像や標高データは国土地理院のタイルAPIを使用しました。国土地理院のデータはタイル状に分割された形で提供されており、今回のように広範囲のデータ(=複数のタイル)が必要なケースではアプリの実装が複雑になりそうでした。そこで、国土地理院に対する複数のタイルAPI呼び出しを集約するAPIサーバをRubyで実装し、Rustアプリからはこれを利用させました。これによりRustアプリは3Dモデルのレンダリングに集中させました。
Rust(OpenGL) Ruby(API) 国土地理院タイルAPI
| | |
| -------------> | |
| | -------------> |
| | <---Tile[1]--- |
| | ... |
| | -------------> |
| | <---Tile[N]--- |
| <--Tile[1-N]-- | |
緯度や経度、標高値を元にOpenGLの座標系に変換するのは特に難しくありませんでした。
(国土地理院のタイルAPIはシンプルで使いやすいです、いつもお世話になっております!)
レンダリングはGL_TRIANGLESなメッシュに地形画像のテクスチャを貼っているだけです。Vertex Array BufferとElement Array Bufferを使用して、GPUで効率よく計算できるように気をつけました。
2. 行動軌跡
GPSの軌跡はGPXファイルから読み出した緯度経度を元に描きました。
レンダリングはGL_LINESで線を引いているだけです。こちらも基本的なフローは地形と同じです。
ただし、GPSの軌跡は地形と異なり時間の経過とともに黄色い線が進んでいくように見せたかったので、描画ループごとに現在のフレーム番号をuniform変数でシェーダに渡し、シェーダ側では次の計算式で現在のフレームより後にレンダリングされるべき頂点を透明にすることで、線が動いているように見せています。(もっとスマートな方法がありそう。。。)
// aFrameは、頂点が表示されるべきフレーム番号(事前に頂点バッファーに含ませておく)
layout (location = 3) in float aFrame;
// aCurrentFrameは、画面に表示中のフレーム番号(uniform変数で渡される)
uniform float aCurrentFrame;
// Alphaをフラグメントシェーダへ渡す
out float Alpha;
void main() {
// ..省略..
// 以下の命令で描画すべきでない頂点を完全な透明(Alpha = 0.0)にする
Alpha = 1.0 - clamp(aFrame / aCurrentFrame, 0.0, 1.0);
// 0.0 < Alpha < 1.0 の場合、黄色い線が曖昧に見える問題を防ぐために、
// clamp(Alpha * 1000.0, 0.0, 1.0) でコントラストを強する
FragColor = vec4(Color, clamp(Alpha * 1000.0, 0.0, 1.0));
まとめ
- 業務をきっかけにOpenGLに興味を持つことができました。
- Rust+OpenGLで地形とGPSの軌跡データを3Dでレンダリングすることができました。
- 3Dプログラミングおもしろいので、もうしばらく楽しめそうです。
おわり