Processingでお手軽3次元データビジュアライズ（人口密度の可視化）

本記事は、[Processing Advent Calendar 2019](https://qiita.com/advent-calendar/2019/processing)用に書きました。

# はじめに
各都道府県の人口密度データを元に、平面地図上にデータに応じた高さを持ったバーを立てていきます。平面地図上でのデータマッピングを行う作品で有名どころでは、takramの「[Planck](https://ja.takram.com/projects/planck/)」でしょうか。そこまでハイクオリティではないですが、サクッとやってみたい方には参考になるかと思います。また、他の地理情報への応用にも参考になるかと思います。

サンプルコードは[こちらからダウンロード](https://files.masakiyamabe.com/visualization_3Dmap.zip)できます。
文中のサンプルコードのファイル名と対応しているので適宜参照しながらお読みください。

__完成図__

![1000-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80141/860e4461-0256-1fb9-969f-f9944a092d30.png)


# 前提条件
Processingのバージョンと使用するライブラリは次の通りです。マウスで視点操作を行うので、ライブラリはPeasyCamを使います。

* Processing3.5.3
* PeasyCam 302

（訂正追記：2019/12/19）
「PeasyCam」のつもりでなぜか「controlP5」と書いていました。なぜ!?w。失礼しました。

# 素材の準備

## データ

データは、都道府県名、都道府県の県庁所在地（緯度・経度）、人口密度を持つcsv形式のファイルです。人口密度は、面積1㎢あたりの人口です（単位は人/㎢）。県庁所在地の場所に人口密度に応じた高さのバーを立てるために緯度、経度を使います。

``` population.csv
city,lat,lon,population
北海道,43.06416667,141.3469444,70.2
青森,40.82444444,140.74,142.4
岩手,39.70361111,141.1525,87.1
宮城,38.26888889,140.8719444,322.3
```

### 緯度と経度

意外とわからなくなるのが緯度と経度です。改めてまとめておきます。

__緯度：latitude__

赤道上は緯度0°、北に向かって緯度は増加、北極点は緯度（北緯）90°

__経度：longitude__

イギリスのグリニッジは経度0°、東に向かって経度は増加、グリニッジの真裏の太平洋上で経度180°東京の緯度経度は、緯度：（北緯）35.69°　経度：（東経）139.69°
[さらに詳しい説明](https://www.gsi.go.jp/KIDS/PAMPHLET/p9.htm)

## 地図画像

使用する地図の画像はProcessingで読み込めればどのようなものでも大丈夫ですが、緯度、経度、それぞれどんな範囲の画像なのかあらかじめ知っておくと、緯度経度から、x座標、y座標へ変換が簡単になります。

例えば、今回使用している画像のサイズと緯度経度の範囲の関係は次のようになっています。

![1000-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80141/6c395e95-a71e-e568-b539-0dc38d8f7ae2.png)

この関係から、例えば、東京の緯度・経度（35.69,139.69）はmap関数を使って次のように地図上のx座標、y座標に変換できます。

```
//東京の経度から地図上のx座標への変換
float x = map(139.69, 122.0, 148.0, 0, 822);
//東京の緯度から地図上のy座標への変換
float y = map(35.69, 24.0, 46.0, 850, 0);
```

（追記：2019/12/08）
この変換方法はあくまで近似的なもので、厳密に正確な位置にマッピングしたい場合や、広範囲をカバーしたい場合には向きません。特にメルカトル図法では緯度とY座標は非線形なので高緯度の場所ではズレが大きくなります。今回のような日本全体で大まかに分布を見るような場合は十分かなと思います。

# 実装

## 準備:座標軸と図の関係

具体的な実装の紹介の前に、座標軸と図の関係を整理しておきます。下図のように、x軸（西→東）、y軸（北→南）、z軸（地表→上空）となります。

![1000-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80141/10c40b1e-c1e4-f1d5-10be-b1436f53f20f.png)

![1000-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80141/dca83be8-b4ab-4ae7-40f2-b9b0e45090a6.png)

## ステップ0: 地図をテクスチャとして張り込むための面を配置

データや画像などの素材を読み込んでおき、画像をテクスチャとして張り込むための面を表示します。
サンプルコード：population_japan_step0.pde

```java:population_japan_step0.pde
import peasy.*; //PeasyCamライブラリをインストールしてインポート
PeasyCam cam;
Table table;
PImage japanmap;

//経度の範囲
float lonFrom = 122.0;
float lonTo = 148.0;
//緯度の範囲
float latFrom = 24.0;
float latTo = 46.0;

void setup() {
  size(1024, 768, P3D);
  cam = new PeasyCam(this, 500);
  //CSVファイルをTableに読み込む
  table = loadTable("population.csv", "header");
  //地図画像を読み込む
  japanmap = loadImage("map-japan.png");
  cam = new PeasyCam(this, 800);
}


void draw() {
  background(0);
  translate(-japanmap.width / 2, -japanmap.height / 2);

  //地図を表示するための面を配置する
  noStroke();
  fill(255);
  beginShape();
  vertex(0, 0, 0);
  vertex(japanmap.width, 0, 0);
  vertex(japanmap.width, japanmap.height, 0);
  vertex(0, japanmap.height, 0);
  endShape();

}
```

これで、読み込む地図画像と同じサイズの面が描画されます。座標軸を図示するとこのようになります。

![1000-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80141/b68e5f76-6e70-a614-f97f-f15903861fdb.png)


## ステップ１：面にテクスチャを張る
前のステップで描画した面のテクスチャに地図画像を指定します。
サンプルコード「population_japan_step1」

```java
void draw() {
  background(0);
  translate(-japanmap.width/2, -japanmap.height/2);
  
  //地図の描画
  noStroke();
  fill(255);
  beginShape();
  texture(japanmap);//テクスチャに読み込んだ画像を指定
  vertex(0, 0, 0, 0, 0);//vertex(x,y,z,textureのu,textureのv)
  vertex(japanmap.width, 0, 0, japanmap.width, 0);
  vertex(japanmap.width, japanmap.height, 0, japanmap.width, japanmap.height);
  vertex(0, japanmap.height, 0, 0, japanmap.height);
  endShape();
  
}
```
テクスチャを指定した後のvertex関数は引数が3つから5つになります。
前のステップと比較して増えた２つの引数は、面の頂点とテクスチャ画像の対応する位置を関連づけるために必要です。図にすると次のようになります。

![1000-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80141/ad42afb7-3401-6736-cfd4-5c1a9ede420f.png)

## ステップ2:緯度・経度を座標に変換する

地図が表示できたので東京の緯度経度をx座標y座標に変換し、地図上にboxを表示してみましょう。
サンプルコード「population_japan_step2」

```java:population_japan_step2.pde
 //東京の緯度は
 float latTokyo = 35.69;
 //東京の経度は
 float lonTokyo = 139.69;
 
 //東京のx座標、y座標は
 float x = map(lonTokyo, lonFrom, lonTo, 0, japanmap.width);
 float y = map(latTokyo, latFrom, latTo, japanmap.height,0);
 
 fill(255,0,0);
 
 //東京の場所にboxを表示
 pushMatrix();
 translate(x, y, 0);
 box(10,10,10);
 popMatrix(); 
```
実行結果
![1000-8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80141/90681298-4c1e-fa1d-2ba2-1cefc6b06603.png)

## ステップ３：boxを使って全都道府県のデータをビジュアライズ

csvから取り出した人口密度をboxの高さに変換して、全都道府県をビジュアライズします。
boxのx,y座標は前のステップのように緯度経度から変換します。box高さはmap関数を使って人口密度を元に変換します。サンプルでは8000人に達すると高さが300pxになります。また、boxのz座標はboxの中心に位置するため、高さがnのboxであれば、z座標はn/2になります。
サンプルコード「population_japan_step3」

```java:population_japan_step3.pde
 //データから全ての行を取り出しながら、boxの高さを人口密度に対応させて描く
 for (int i=0; i<table.getRowCount(); i++) {
   float lat = table.getRow(i).getFloat("lat");
   float lon = table.getRow(i).getFloat("lon");
   float population = table.getRow(i).getFloat("population");
   float x = map(lon, lonFrom, lonTo, 0, japanmap.width);
   float y = map(lat, latFrom, latTo, japanmap.height, 0);
   
   //barの高さをmap関数で計算。1人ならば高さ1px、8000人ならば300px
   float bar = map(population, 1, 8000, 1, 300); 
   pushMatrix();
   //地表からの高さがNのboxを描くとすると、boxの中心のz座標はN/2
   translate(x, y, bar/2); 
   box(5,5,bar);
   popMatrix();
 } 
```
実行結果
![1000-9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80141/05d5114a-55e2-3113-8460-b293dbc33dc6.png)

## ステップ4: 仕上げ
あとは、色相をデータに対応させたり、突出した箇所は文字でデータを表示するなどして仕上げましょう。
サンプルコード「population_japan」

```java
for (int i=0; i<table.getRowCount(); i++) {
    float lat = table.getRow(i).getFloat("lat");
    float lon = table.getRow(i).getFloat("lon");
    float population = table.getRow(i).getFloat("population");
    //都市名を取得
    String cityname = table.getRow(i).getString("city"); 
    float x = map(lon, lonFrom, lonTo, 0, japanmap.width);
    float y = map(lat, latFrom, latTo, japanmap.height, 0);
    float bar = map(population, 1, 8000, 1, 300); 

    //色相をmap関数で計算。
    float hue = map(population, 1, 8000, 120, 0); 
    fill(hue, 255, 255, 180);
    pushMatrix();
    translate(x, y, bar/2); 
    box(2, 2, bar);
    popMatrix();

    //人口密度の高い都市名を表示
    if (population > 500) {
      pushMatrix();
      fill(0, 0, 255);
      translate(x, y, bar);
      rotateX( radians(-90) );
      text(cityname + ":" +population, 0, 0);
      popMatrix();
    }
  }
```

## 完成
![1000-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80141/014a824d-36cf-688e-88cf-5ee0a30f9ce7.png)

# バリエーション

## boxを使わない
サンプルコード「population_japan_vertex」
boxはレンダリングコストが高いので、代わりに`beginShape(LINES);`と`vertex();`でバーの部分を描画する方法もあります。この方法では、頂点ごとに固有の色を指定できるため、バーの先端は明度が高く、下端では低いというようなグラデーション表現ができます。

![1000-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80141/1059a092-0d2e-755e-c784-f13a7ab31d6a.png)

```java:population_japan_vertex.pde

  beginShape(LINES);
  for (int i=0; i<table.getRowCount(); i++) {
    ...中略...

    //barの上端の色相をmap関数で計算。
    float hueTop = map(population, 1, 7000, 120, 0);
    //barの下端の色相
    float hueBottom = 120;

    //barの上端のvertexの色と座標
    stroke(hueTop, 255, 255, 180);
    vertex(x, y, bar);

    //barの下端のvertexの色と座標
    stroke(hueBottom, 255, 60, 180);
    vertex(x, y, 0);
  }
  endShape();
```

ただし、` beginShape(LINES);`と`endShape();`の間に`text();`をはさみ込めないので、文字でデータを表示したい場合は、線の描画とは別にループを回します。（よりスマートな実装方法は他にあるかもしれません。）

## 高層ビル

サンプルコード「population_japan_building」

`beginShape(QUADS);`を使って高層ビルのような見た目に変更できます。

![1000-13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80141/8b61f73c-7871-0a1f-04e9-e0120ff67fb7.png)


```java:population_japan_building.pde
  beginShape(QUADS);
  for (int i=0; i<table.getRowCount(); i++) {
    ...中略...
    //barの高さに達するまで、階層を描く。
    for (float j=0; j < bar; j = j+ 2) {
      
      //現在の階層の色相と明るさをmap関数で計算。
      float hue = map(j, 1, 300, 120, 0);
      fill(hue, 255, 255, 180);
      stroke(hue, 255, 255, 180);

      //階層の四角形の4頂点を指定
      vertex(x-2, y-2, j);
      vertex(x+2, y-2, j);
      vertex(x+2, y+2, j);
      vertex(x-2, y+2, j);
    }
  }
  endShape();
```

こういった、「何かに見立てる」というアプローチは、ビジュアライズでロジカルに訴求するだけでなく、感性にも働きかけるようにする際にはとても有効です。

「PopulouSCAPE（ポピュラスケープ）」という作品を参考にしました。

__PopulouSCAPE（ポピュラスケープ）__
[https://vimeo.com/populouscape](https://vimeo.com/populouscape)

![1000-14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80141/0f1d3ed4-5e20-37ad-3bdb-0283dd97cc87.png)


## バーをアニメーションさせる

サンプルコード「population_japan_vertex_animation」

`lerp();`を使って、徐々にバーの高さを伸ばします。サンプルでは、キーを押している間バーが伸びます。スペースキーでリセットされます。

![f189563720b64faa2f792ab94fc266d4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80141/2b1a73f9-f4c7-01cd-937c-3a1950841c8d.gif)

```java:population_japan_vertex_animation.pde

  for (int i=0; i<table.getRowCount(); i++) {
    ...中略...
    float bar = map(population, 1, 7000, 1, 300); 
    float hueTop = map(population, 1, 7000, 120, 0);
    float hueBottom = 120;

    //barの高さのt%の値をlerp関数で計算
    float tbar = lerp(0, bar, t);
    //hueTopの色相のt%の値をlerp関数で計算
    float thue = lerp(hueBottom, hueTop, t);

    stroke(thue, 255, 255, 180);
    vertex(x, y, tbar);
    stroke(hueBottom, 255, 60, 180);
    vertex(x, y, 0);
  }
  endShape();

  if ( keyPressed ) {
    if (t < 1.0) {
      t = t + 0.01;
    }
    if (key ==' ') {
      t = 0;
    }
  }

```

# 終わりに

以上です。
今期の多摩美情報デザイン学科2年生向けの演習授業で使用したネタをアレンジしました。

このコードをベースに、地震の震源データや、旅客機のフライトデータなどを使用してビジュアライズできたので応用範囲は広いかなと思います。

学生の皆さんは最終課題、頑張ってください。