RUST言語 入門日記 (3) 【ISUCON用計測ミドルウェア編】

Rustについて、勉強した内容を少しでもアウトプットしたいため、ISUCON用計測ミドルウェアactix-web-isucon-measured を作成しました。

使い方についてはgithubのサンプルを参照してください。
https://github.com/sengine-xyz/actix-web-isucon-measured

早速ISUCON11予選問題で確認します。

ISUCON11予選のRUST実装にそのまま適応するとコンパイルエラーになるため、少し修正が必要です。

修正 1 Cargo.toml

actixのbetaが使われているため、図のようにbetaを消し安定版を使います。

修正 2 main.rs

図のように2か所を修正すると、コンパイルが通ります。

ミドルウェア適応

基本サンプル通りに使えば問題ありません。

ベンチして結果確認。

/measured_tsvへアクセスします。

エクセルにコピペするとわかりやすいかと思います。

最後に

ISUCONを簡単に練習できるよう、過去問をgithubに公開し、便利ツール作ってくれた方々に感謝します。

ディフォルトでtsvを出力する実装があるが、計測データが直アクセスできるため、サービス登録する時JSONなど他のフォーマットも実装出来ます。

ISUCON12もactix-webフレームワークだと使いたいと思います。

そして誰かの役に立つと幸いです。

RUST言語 入門日記 (2)

RUST言語のAsync .await Future について本日の纏め。

RUSTはAsync .await Futureの定義やfuturesといった便利ツールを提供するが、asyncを実行するruntimeはtokioやasync-stdなど外部ライブラリになる。

Async .awaitについて

1. Async関数はFutureを返すだけであり、futures::executor::block_on やtokioと言ったexecutorがない限り実行されない。

1
2
3
4
5
6
async fn do_something() { 
println!("go go go !");
}
fn main() {
do_something();
}

2. .awaitは順番を保障するがスレッドをブロックしない。コンパイラがスマートにやってくれる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use futures::executor::block_on;

async fn hello_world() {
hello_cat().await;
println!("hello, world!");
}

async fn hello_cat() {
println!("hello, kitty!");
}
fn main() {
let future = hello_world();
block_on(future);
}

Futureについて

簡単なFuture

executorがpollを呼び出し、たくさんのFutureを上手く調達する。
pollは何回も呼ばれ、Pendingの場合次のwakeがセットされ、Readyの場合Futureが終了する。

1
2
3
4
5
6
7
8
9
trait SimpleFuture {
type Output;
fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;
}

enum Poll<T> {
Ready(T),
Pending,
}

Pin Unpin !Unpinについて。

Pinはstruct。Unpinはtrait。!UnpinはUnpinをではないこと。

Rustのなか、あるインスタンスをmoveさせたくないときにPinを使う。
構造は単純で下記の通り。

1
2
3
pub struct Pin<P> {
pointer: P,
}

そのためPin<&mut T> , Pin<&T> , Pin<Box> どれでもいい。
ただTが!Unpinではないといけません。UnpinのものをPinしても意味がない。
例え、Pin<&mut u8>は&mut u8とほぼ同じでPinを使う意味がない。

RustではほどんどものがUnpinになる。しかしFutureは!UnpinであるためPinと組み合わせて書く場合がよくある。

普通のstructを!Unpinにしたい時はPhantomPinnedを使う。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
use std::pin::Pin;
use std::marker::PhantomPinned;

#[derive(Debug)]
struct Test {
a: String,
b: *const String,
_marker: PhantomPinned,
}

impl Test {
fn new(txt: &str) -> Pin<Box<Self>> {
let t = Test {
a: String::from(txt),
b: std::ptr::null(),
_marker: PhantomPinned,
};
let mut boxed = Box::pin(t);
let self_ptr: *const String = &boxed.as_ref().a;
unsafe { boxed.as_mut().get_unchecked_mut().b = self_ptr };

boxed
}

fn a(self: Pin<&Self>) -> &str {
&self.get_ref().a
}

fn b(self: Pin<&Self>) -> &String {
unsafe { &*(self.b) }
}
}

pub fn main() {
let test1 = Test::new("test1");
let test2 = Test::new("test2");

println!("a: {}, b: {}",test1.as_ref().a(), test1.as_ref().b());
// std::mem::swap(test1.get_mut(), test2.get_mut()); //pin されてるためswap出来ない。
println!("a: {}, b: {}",test2.as_ref().a(), test2.as_ref().b());
}

RUST言語 入門日記 (1)

RUST言語を再度入門する時間があったので、大事なところをメモする。

イディオム

1. 関数のパラメータは極力所有権のない参照型にする。

&Stringではなく&str
&Vecではなく&[T]
&Boxではなく&T

暗黙的な参照外しがあるため、この方が汎用性が高くなる。

2. 文字列結合する時は極力format!を使う。

動的に変わる文字列の場合はpush_strにするしかない。

3. Rustはコンストラクターがないため、関連函数newよりオブジェクトを作る。

Default traitを実装すると、***_or_defaultが使えるようになる。例えば、
Option::unwrap_or_default()

4. コレクションはスマートポインター。

Deref traitを実装し、コレクションからスライスへ変換する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::ops::Deref;

struct Vec<T> {
data: RawVec<T>,
//..
}

impl<T> Deref for Vec<T> {
type Target = [T];

fn deref(&self) -> &[T] {
//..
}
}

5. Drop traitの実装は、関数がエラー終了や、panicじも実行される。

しかし、dropの中にpanic発した場合、スタックの後続dropは実行されないため、dropの実装はpanicが発生しないよう慎重に実装すべきである。

6. mem::{take(), replace()}を使って、所有権を残し、cloneを省く。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::mem;

enum MyEnum {
A { name: String, x: u8 },
B { name: String }
}

fn a_to_b(e: &mut MyEnum) {
if let MyEnum::A { name, x: 0 } = e {
// this takes out our `name` and put in an empty String instead
// (note that empty strings don't allocate).
// Then, construct the new enum variant (which will
// be assigned to `*e`).
*e = MyEnum::B { name: mem::take(name) }
}
}

Default traitが実装されてない場合、mem::replaceを使う。

7. トレーイト等価実装。

トレーイト 関数1 関数2
std::fmt::Display ***.to_string()
std::str::FromStr ***.parse()
std::convert::From ***::from() ***.into()
std::convert::TryFrom ***::try_from() ***.try_into()

8. for in と Iterator

into_iter
iter
iter_mut
がある。
for x in vec の場合 ディフォルトinto_iterが呼ばれる。
into_iterの場合、所有権がmoveされるため、元のvecが使えなくなる。
iterの場合参照を取る。

1
2
3
4
5
6
7
8
9
10
fn main() {
let names = vec!["Bob", "Frank", "Ferris"];

for name in names.into_iter() {
match name {
"Ferris" => println!("There is a rustacean among us!"),
_ => println!("Hello {}", name),
}
}
}

8. matchについて

1
2
3
match color {
Color::RGB(r, g, b) => println!("Red: {}, green: {}, and blue: {}!", r, g, b),
_ => println!("It doesn't matter what they are"),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

fn some_number() -> Option<u32> {
Some(42)
}

fn main() {
// 参照など
let reference = &4;
match reference {
&val => println!("Got a value via destructuring: {:?}", val),
}

match *reference {
val => println!("Got a value via dereferencing: {:?}", val),
}

let _not_a_reference = 3;
let ref _is_a_reference = 3;

let value = 5;
let mut mut_value = 6;

match value {
ref r => println!("Got a reference to a value: {:?}", *r),
}

match mut_value {
ref mut m => {
*m += 10;
println!("We added 10. `mut_value`: {:?}", m);
},
}

// 条件付き
let pair = (2, -2);
match pair {
(x, y) if x == y => println!("These are twins"),
(x, y) if x + y == 0 => println!("Antimatter, kaboom!"),
(x, _) if x % 2 == 1 => println!("The first one is odd"),
_ => println!("No correlation..."),
}

match some_number() {
Some(n @ 42) => println!("The Answer: {}!", n),
Some(n) => println!("Not interesting... {}", n),
_ => (),
}

}

9. Copyについて。

Clone は Copy の super trait。
Copy実装には Cloneの実装が必須。

CopyとDropは同時不可。

1
2
3
pub trait Copy: Clone {
// Empty.
}

ISUCON11 予選 参加しました(RUST)

ISUCON11 予選 参加しました(RUST)

チーム名sengine(エスエンジン)です。一人チームでした。

「ブログを書くまでがISUCONです!」なので書きます。

参加するまで

ISUCON8からISUCONのこと知りました。

普段の業務ではJAVAがメインの言語です。ISUCONではJAVAの実装はないため、GO,TypeScript,RUSTなどを
勉強しながらで参加しました。

  • ISUCON8予選 Golangを2週間の入門レベルで参加しました。
    初めてのISUCONだったので手も足も出ない状態でした。
    頭真っ白で終了したことだけ印象に残っています。
    結果はもちろん敗退です。

  • ISUCON9予選 知り合いと2人でチャレンジしました。
    フロントエンドのJavaScriptの開発経験があるので、NodeJSのJavaScript実装があることを期待していました。しかしNodeJSの参考実装はTypeScriptだったので、またお手上げ状態になりました。
    あれこれやってもスコア上がらないので途中で諦めモードになりました。
    また敗退です。

  • ISUCON10予選 申請が遅れ参加出来ませんでした。

  • ISUCON11予選 RUST言語を勉強し始めたので、RUSTでチャレンジ出来ました。

やったこと

RUSTの初期スコア4000ぐらいでした。

1. SQLを見ながらテーブルにindex追加。

isu_conditionテーブルにてjia_isu_uuidと(jia_isu_uuid,timestamp)のindexを追加しました。

2. #[actix_web::post(“/api/condition/{jia_isu_uuid}”)] にて複数のINSERT文があるため1つに纏める。

Spring JDBCのbatchUpdateのようにやりたかったが、RUSTでは似た実装が見つからず、試行錯誤で何とか実装できました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
let mut sql = String::from("INSERT INTO `isu_condition` (`jia_isu_uuid`, `timestamp`, `is_sitting`, `condition`, `message`) VALUES ");
let mut times = Vec::new();
for (idx, cond) in req.iter().enumerate() {
let timestamp: DateTime<chrono::FixedOffset> = DateTime::from_utc(
NaiveDateTime::from_timestamp(cond.timestamp, 0),
JST_OFFSET.fix(),
);
times.push(timestamp.naive_local());
if idx == req.len() - 1 {
sql.push_str("(?, ?, ?, ?, ?)");
} else {
sql.push_str("(?, ?, ?, ?, ?), ");
}
}

let mut query = sqlx::query(sql.as_str());

for (idx, cond) in req.iter().enumerate() {

if !is_valid_condition_format(&cond.condition) {
return Err(actix_web::error::ErrorBadRequest("bad request body"));
}
query = query
.bind(jia_isu_uuid.as_ref())
.bind(&times[idx])
.bind(&cond.is_sitting)
.bind(&cond.condition)
.bind(&cond.message);
}

query.execute(&mut tx)
.await.map_err(SqlxError)?;

3. 各所にCacheControl追加。

  • #[actix_web::get(“/api/isu/{jia_isu_uuid}/icon”)]
    にて86400秒

    1
    Ok(HttpResponse::Ok().insert_header(CacheControl(vec![CacheDirective::MaxAge(86400u32)])).body(image))
  • #[actix_web::get(“/api/isu/{jia_isu_uuid}/graph”)]
    にて1秒

    1
    Ok(HttpResponse::Ok().insert_header(CacheControl(vec![CacheDirective::MaxAge(1)])).json(res))
  • #[actix_web::get(“/api/condition/{jia_isu_uuid}”)]
    にて1秒

    1
    Ok(HttpResponse::Ok().insert_header(CacheControl(vec![CacheDirective::MaxAge(1)])).json(conditions_response))

4. isuテーブルに入れていた画像をファイルにする。

  • #[actix_web::post(“/initialize”)]
    にて
    1
    2
    3
    4
    5
    6
    7
    8
    let isu_list: Vec<Isu> = sqlx::query_as("SELECT * FROM `isu`")
    .fetch_all(pool.as_ref())
    .await
    .map_err(SqlxError)?;
    for isu in isu_list {
    let file = format!("{}/{}_{}.jpeg", IMG_PATH, isu.jia_isu_uuid, isu.jia_user_id);
    fs::write(file, isu.image).unwrap();
    }

また

#[actix_web::post(“/api/isu”)]

#[actix_web::get(“/api/isu/{jia_isu_uuid}/icon”)]
にて合わせた修正をしました。

大分時間かかった割にスコアが全く上がりませんでした。
椅子の数が少ないからだと思います。

5. SQL箇所でlimit 追加。

終了時間が迫ったので強引なやり方をしました。

  • get_isu_conditions_from_db関数にて

    1
    2
    3
    4
    5
    6
    7
    8
    9
             sqlx::query_as(
    - "SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = ? AND `timestamp` < ? AND ? <= `timestamp` ORDER BY `timestamp` DESC",
    + "SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = ? AND `timestamp` < ? AND ? <= `timestamp` ORDER BY `timestamp` DESC limit 25",
    )

    sqlx::query_as(
    - "SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = ? AND `timestamp` < ? ORDER BY `timestamp` DESC",
    + "SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = ? AND `timestamp` < ? ORDER BY `timestamp` DESC limit 25",
    )
  • #[actix_web::get(“/api/trend”)]

    1
    2
    3
    4
    5
                 let conditions: Vec<IsuCondition> = sqlx::query_as(
    - "SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = ? ORDER BY timestamp DESC",
    + "SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = ? ORDER BY timestamp DESC limit 1",
    )
    )
  • generate_isu_graph_response関数にて

    1
    2
    3
    4
    5
    6
    7
    8
         let mut rows = sqlx::query_as(
    - "SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = ? ORDER BY `timestamp` ASC",
    + "SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = ? AND `timestamp` < ? + interval 1 day AND ? - interval 1 hour <= `timestamp` ORDER BY `timestamp` ASC",
    )
    .bind(jia_isu_uuid)
    + .bind(graph_date.naive_local())
    + .bind(graph_date.naive_local())
    .fetch(tx);

やった、スコアが72315まで上がりました。

やり残したこと

mariaDBを2号サーバーにしたかったが、設定が上手くいかない。1号サーバーからログインできず、時間切れで断念しました。

RUSTの感想

RUSTはGCがなく、メモリをキッチリ制御できる言語です。
しかし他のスクリプト言語と比べるとコンパイルが遅く、ベンチマーク走らせるまでに時間かかります。
今回のシステムはコンパイルに50秒ぐらいかかった。

最後に

ISUCONは大変素晴らしいコンテストです。運営の方々に感謝します。
また、試合に集中させてくれた妻に感謝します。

追伸

結果敗れました。

最終順位は43でした。
悔しいが、初めて決勝進出に近づくことが出来ました。
RUSTとmysqlを磨いて、来年も頑張ります。

TODO

  1. actix_web のアクセスを計測するツール作る。
  2. mysqlの設定に関する勉強。

Mapbox 入門日記(5)ベクトルタイル

Mapbox 入門日記(5)ベクトルタイル

ベクトルタイルについて

地図データは一般的に大きくなりがちです。
そのため、ブラウザに地図データを描画するには、常にネットワーク
帯域、CPU、やメモリとの戦いになり、バランスを取ることが大事です。

昔はサーバーサイドで、予めタイル画像を作成し、クライアント側で
タイル画像を順番に並べるのが普通でしたが、ダイナミックに描画
スタイルを変更できないなど欠点がありました。

HTML5のcanvasとWebGLの普及により、近年はGEOデータをそのまま
クライアントサイトに渡しjavascritpで描画する方法が進んでいます。

しかし、クライアントサイトで描画する場合、データが大き過ぎる問題
に直面します。特にズームレベルを小さくして、全日本や全球レベルの
地図を表示る時にデータが膨大になりがちです。

こういった問題を解決するためにMapbox社からベクトルタイルが定義
されたわけです。

関連定義や説明は下記のリンクから参照してください。

日本語のベクトルタイル仕様書
https://github.com/madefor/vector-tile-spec/blob/master/2.1/README.md

mapboxドキュメントの説明
https://docs.mapbox.com/vector-tiles/specification/

大体の流れは下記の通りです。

  1. GeoJSONなどGEOデータをメルカトル座標に変換。
  2. そのデータをタイル分割して、4096×4096の格子に決まったルールで変換(ある意味描画)する。
  3. 4096×4096の格子映されたjsonデータをProtocol Buffersフォーマットに圧縮。

※定義上は必ずしもメルカトル座標ではない。
※格子の密度(4096)は変更可能。

Mapboxとベクトルタイル

Mapboxはベクトルタイルを描画するために生まれたものと言っても過言ではない
でしょう。

Mapboxはサーバーサイドからベクトルタイルを取得し、WebGLにより高速な描画を
実現します。

ベクトルタイルの作成

ベクトルタイル作成するためのいろいろなライブラリがあります。

各プログラム言語で使えるライブラリがあれば、コマンドライン
で静的にPBFファイルに変換してくれるツールもあります。

https://github.com/mapbox/awesome-vector-tiles

tippecanoe はコマンドラインで、GeoJSONからmbtilesファイル作成できます。
mbtilesファイルはsqlite3により、たくさんのベクトルタイルを1つのファイルに纏めたものです。
さらにmb-utilコマンドでmbtilesを展開して、httpサーバーに公開できます。

1
2
3
tippecanoe -o out.mbtiles -Z6 -z13 --drop-densest-as-needed --no-tile-compression test.geojson

mb-util --image_format=pbf out.mbtiles tiles

最後に

思いつくまま書きました。
間違っているところがありましたらTwitter経由でご指摘下さい。

Mapbox 入門日記(4)カスタムレイヤ

Mapbox 入門日記(4)カスタムレイヤ

Mapboxカスタムレイヤ

Mapboxでは、GLSL書ける type: ‘custom’ が用意されています。

使い方は公式サンプルサイト
https://docs.mapbox.com/mapbox-gl-js/example/custom-style-layer/
を参照してください。

基本的にonAddでシェーダーを用意して、renderでdrawします。

カスタムレイヤの問題点

renderで受け取るmatrixですが、メルカトル座標を描画するためのもの
です。

mapbox内部では、ベクトルタイルを描画するためにベクトルタイル座標
をベースにした描画を行っています。

ズームを拡大すると、精度の問題により、カスタムレイヤのブレが生じます。

詳細はこちらのissuesで議論されています。
https://github.com/mapbox/mapbox-gl-js/issues/7268

カスタムレイヤよりカスタムデータ

mapbox-gl-js自体はベクトルタイルを描画するためにいろいろのオプションが
あります。
一方描画(GLSL)をカスタマイズするためのcustomレイヤの機能が限られています。

そのため、サーバサイトで、独自でベクトルタイルを作って、mapbox-gl-jsに描画させるのが
正しいアップローチかと思います。

Mapbox 入門日記(3)基本概念

Mapbox 入門日記(3)基本概念

Mapbox基本概念

Mapboxでは、Layer(StyleLayer)とSourceはとても重要な部分になります。

Layer(StyleLayer)

mapboxの描画方法になります。
現時点下記
circle,
heatmap,
hillshade,
fill,
fill-extrusion,
line,
symbol,
background,
raster,
custom
10種類あります。

それぞれStyleLayerを継承し、定義されています。

Source

layerを描画する時に使われるデータです。
vector,
raster,
raster-dem,
geojson,
video,
image,
canvas
7種類あります。

SourceCacheはSourceのコンテナーであり、Sourceをタイル形式に変換し、そのタイルをCacheします。

Bucket

BucketはSourceがタイルに分割されたgeoデータをGLのvertexとして使えるBufferなどに変換し、キープしています。

GLSL

実際webglで描画に使うShaderスクリプト。

Draw

具体的に描画コードの入口はpainter.jsファイルの下記の部分です。

painter.js

1
2
3
4
5
6
7
renderLayer(painter: Painter, sourceCache: SourceCache, layer: StyleLayer, coords: Array<OverscaledTileID>) {
if (layer.isHidden(this.transform.zoom)) return;
if (layer.type !== 'background' && layer.type !== 'custom' && !coords.length) return;
this.id = layer.id;

draw[layer.type](painter, sourceCache, layer, coords, this.style.placement.variableOffsets);
}

まとめ

MapboxはEvent Drivenの形で、データ処理、描画しています。
最新のjavascript, webGL, webWorker, GISの技術を多用しています。
そのため、完全に理解するには多少時間がかかります。
今後できるだけ詳しく書いて行きたいと思います。

Mapbox 入門日記(2)使い方

Mapbox 入門日記(2)使い方

Mapboxは基本Style, Source, Layerを設定しながら使います。

Mapboxを使うにはまずmapboxgl.Mapのインスタンスを作成します。
この時、いろいろと設定出来ます。

その中styleの設定がとりわけ重要になります。

下記の図のように、styleにはsourceとlayersの設定が含まれます。

sourceは地図で使うリソースを記入します。geojson, 画像tile, vector tileなどなど。
layersにはsourceを使った描画方法を記入します。backgroundなど、sourceを使わないレイヤも
ありますが、基本sourceに登録された、データを
circle,
heatmap,
hillshade,
fill,
fill-extrusion,
line,
symbol,
background,
raster,
custom
のいずれかの方法で描画することを設定します。

具体的には下記のサンプルを参照してください。

トークン無しで使えるMapboxデモ。

当サイトでは以前Mapboxを利用にはTokenが必須と書きましたが
厳密にはmapbox APIを利用するならTokenが必須であり、mapbox-gl-jsを使うだけであれば、
Token無しでできます。

そのため、今回はトークン無しmapbox-gl-jsを使えるデモを作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var map = new mapboxgl.Map({
container: 'map',
zoom: 10,
center: [139.767125, 35.681236],
style: {
"version": 8,
"sources": {
"tilemap": {
"type": "raster",
"tiles": ["https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png"],
"tileSize": 256,
}
},
"layers": [{
"id": "background",
"type": "background",
"paint": {
"background-color": "rgb(125,125,125)"
}
}, {
"id": "tilemap",
"type": "raster",
"source": "tilemap",
"minzoom": 0,
"maxzoom": 18
}]
},
});

デモサイトはこちらです。

トークン無しで使えるMapboxデモ

もちろんmapbox-gl-jsでは、map.addLayerとmap.addSourceの動的に追加する方法も用意されています。

必要に応じて使えます。

Mapbox 入門日記(1)環境構築

Mapbox 入門日記(1)環境構築

Mapboxについて

前から気になっていましたが、今回は本気で勉強することになりした。

Mapboxはwebglを活用して、vectortileなどのデータをブラウザで高速に描画するライブラリです。
使うには事前に登録とトークンが必要です。

環境構築

やり方はlinuxのVMにmapbox-gl-jsをインストールし、vscodeのRemote Developmentを使て
コードを読んだり、修正したり、テスト実行したりるす。

VMはvirtualboxでUbuntu64_18.04を入れました。

本家マニュアル
https://github.com/mapbox/mapbox-gl-js/blob/master/CONTRIBUTING.md

本家githubにも書いてありますが、こちらのやり方は下記の通りです。

rootユーザで実行しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#Install lib
apt-get update
apt-get install build-essential git nodejs libglew-dev libxi-dev


#Install yarn
curl -o- -L https://yarnpkg.com/install.sh | bash


#Install clone
git clone git@github.com:mapbox/mapbox-gl-js.git

cd mapbox-gl-js
yarn install

vscodeの設定

Remote Development

Remote DevelopmentのExtensionをインストールします。
手順はネットでいっぱいあるため割愛します。

Remote Developmentを使って、linuxにつなぐため
C:/Users/{user}/.ssh\configファイルに
下記のような設定を追加する。

1
2
3
4
5
6
Host vbox.ubuntu64_18_14
User root
HostName xxx.xxx.xxx.xxx
Port 22
IdentityFile C:/Users/{user}/.ssh/key.file
BatchMode yes

key.fileやIPを自分の環境と合わせて変更してください。

flowtype

Remote Developmentよりubuntu64_18_14に接続出来たら、cloneしたmapbox-gl-jsフォルダを開きます。

maxbox はflowtypeを使って開発されています。
そのため、リモートワークスペースにvscodeのExtension
「Flow Language Support」と「vscode-flow-ide」をインストールします。

ディフォルトのvscodeではエラーいっぱい出ますのでjavascript.validate.enableの設定をfaseにする。

setting.json

"javascript.validate.enable": false

debug

debug起動

1
2
3
4
5
6
7
8
9
10
#token作成
MAPBOX_ACCESS_TOKEN=yourkey yarn run build-token

#コード修正後または起動前にはビルドコマンドでコンパイルする。
yarn run build-dev

#サーバーを起動する。
yarn run start-server

#起動後はhttp://xxx.xxx.xxx.xxx:9966/debug にアクセスして確認できる。

終り

ひとまず、環境が整えました。これからはコードを分析したり、カスタマイズ
したりして、理解を深めていきたいと思います。

ピュアなwebglで描画する3D日本の地形地図

3D日本の地形地図

今回はwebglについて勉強していた時の成果物
日本の地形地図
に関するの内容を書きます。
実際2年も前に作ったものであるため詳細は忘れている部分もありますが、
思い出せるところまで書きます。

また私の勉強不足により、間違っている内容もあるかもしれないので、
間違っているところをTwitterで教えて頂けると嬉しいです。

webglについて

webglはブラウザの上でグラフィックカードを使った3D描画のためのインターフェースになります。
近年モダンなブラウズがほとんど対応しています。

webglをベースにした有名なライブラリはthree.jsになります。しかし本当にwebglを勉強したい方
はthree.jsを介さずに、直javascriptでwebglを基本から練習することをおすすめします。

その理由は、グラフィックカードを動かしているのはGLSL (OpenGL Shading Language) だからです
グラフィックカードで実行されるプログラムはCPUで実行される一般的(C, java, javascript)な言語
と違って、vertex shaderとfragment shaderに書かれて通りに動作します。

OpenGLを触れていないプログラマーにはこの部分はかなりハードルがあると思います。実際私もそうだった。

日本の地形の元データ

地形データは国土地理院の標高タイルタイルを元に作成します。
標高タイルの詳細は下記のサイトを参照してください。

https://maps.gsi.go.jp/development/demtile.html

写真地図は下記のタイルを使います。
https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg

日本の地形描画の基本的な考え方

3Dの描画はcanvas 2dと違って、円や、四角、ポリゴンを直接描画できません。
webglは基本すべてが三角の頂点データになります。

そのため、下記の手順で描画します。

  1. まず地理院の標高タイルを取得してから、格子状に三角の頂点データを作成します。

  2. その後、各三角頂点データの間のピクセルを写真地図タイルのピースで貼り付けます。

この2つが出来たら、後はデータを渡して、webglで描画させるだけです。

実際描画のコードの抜粋を貼ります。

vertex shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
attribute float      gridIndex;
uniform mat4 mvpMatrix;
uniform vec4 gridInfo;
uniform sampler2D demTex;
varying vec2 colorCoord;
void main(void) {
int idx = int(gridIndex - 1.0);
int j = idx / int(gridInfo.z);
int i = idx - j * int(gridInfo.z);
vec2 XY = vec2(float(i) * gridInfo.x - gridInfo.x * gridInfo.z / 2.0, gridInfo.y * gridInfo.w / 2.0 - float(j) * gridInfo.y);
vec2 tp = vec2(float(i)/gridInfo.z + 0.5/gridInfo.z, 1.0 - float(j)/gridInfo.w - 0.5/gridInfo.w);
float Z = getZ(texture2D(demTex, tp));
gl_Position = mvpMatrix * vec4(XY, Z, 1.0);
colorCoord = tp;
}

fragment shader

1
2
3
4
5
6
precision mediump    float;
varying vec2 colorCoord;
uniform sampler2D mapTex;
void main(void) {
gl_FragColor = texture2D(mapTex, colorCoord) * vec4(1.0, 1.0, 1.0, 1.0);
}

座標の計算

webglによる3D描画は座標変換がとても重要です。
しかも3次元の座標変換はても複雑になりがちです。
そこで数学の行列の基礎が問われます。
行き詰まった時は、原点に戻り、座標変換のステップを一つずつ追って見るがいいでしょう。

最後に

ある程度shaderを書けて、座標の計算も慣れてから、three.jsを使うとwebglがとても
楽しくなって来ると思います。

webglに関しては、書きたい内容はは沢山ありますので、また整理出来てから書きたい
と思います。