GONDWANA開発ブログ

五大開発株式会社のGONDWANA開発に関する公式ブログです。

ソースデスクリプターの応用例: "G3: STD * RELIEF" の作り方

GONDWANAのタイルマップは複数の「レイヤー」があり、それぞれの「レイヤー」には1つの「標高ソース」と複数の「アピアランスソース」があり、この階層構造によって実行時に異なる複数のソースをユーザーの望む合成方法を選択してGPUで高速に合成処理を行い、可視化する仕組みです。

今回はこのタイルマップの「ソース」について少し応用的な解説をしたいと思います。GONDWANAのタイルマップの「標高ソース」あるいは「アピアランスソース」は何れもユーザーが自由に作成、編集できるよう仕組みになっています。

GONDWANA の asset/script/source_descriptor/ に「標高ソース」は height/ 「アピアランスソース」は appearance/ ディレクトリーにそれぞれの定義がスクリプトファイルの形式で入っています。一部のユーザーから「予めGSI-STDとGSI-RELIEFを乗算合成したアピアランスソースを作りたいがどうしたら良いかわからないので参考となるスクリプトを1つ用意して欲しい」といった具合の要望を頂きました。そこで、この記事ではこの要望に答えるスクリプトの書き方を解説する事にしました。

↑このように、この記事で作成する "G3: std * relief" が完成すると、GONDWANAのアピアランスソースとして選択でき、GSI-STDとGSI-RELIEFを1つのソースとして合成したタイルを扱えるようになります。

それでは、早速作ってみましょう。

先ずは、 appearance/ に g3_experimental_std_x_relief.lua としてファイルを追加する事にしましょう。(実際にユーザー自信がスクリプトを新規に作成する場合は、ディレクトリーやファイル名をGONDWANA標準で提供されるものと混ざらないように分けて作った方が良いでしょう。) .lua の拡張子が示すように、現在の GONDWANA のソースデスクリプターLua 言語のスクリプトに対応しています。将来的には PythonJavaScript などより一般性の高い言語エンジンにも対応したいと考えていますが、現在は実行速度や開発都合等から Lua 言語のみの対応です。

最初に今回作成するコードの全体像を示してしまう事にします。 Lua 言語は少し勉強すれば誰にでも扱えるようなシンプルな言語の1つです。パワーユーザーでなくとも臆さずに挑んでみて下さい。

url_pattern = "g3.synthesis://g3_experimental_std_x_relief/{z}/{x}/{y}"
sub_pattern = "http://cyberjapandata.gsi.go.jp/xyz/{w}/{z}/{x}/{y}.png"
sub_key_std    = "std"
sub_key_relief = "relief"

function get_name()
  return "g3_experimental_std_x_relief"
end

function get_appearance_name()
  return "G3.Experimental: std * relief"
end

function get_parameter_type_string()
  return "x_y_zoom_type"
end

function get_data_type_string()
  return "u8vec4_tile_type"
end

function zoom_min()
  return 5
end

function zoom_max()
  return 15
end

function is_heavy()
  return false
end

function generate_url( x, y, zoom )
  local url = string.gsub( url_pattern, "{x}", x, 1 )
  local url = string.gsub( url        , "{y}", y, 1 )
  local url = string.gsub( url        , "{z}", zoom, 1 )
  return url
end

function generate_sub( x, y, zoom, sub_key )
  local url = string.gsub( sub_pattern, "{x}", x, 1 )
  local url = string.gsub( url        , "{y}", y, 1 )
  local url = string.gsub( url        , "{z}", zoom, 1 )
  local url = string.gsub( url        , "{w}", sub_key, 1 )
  return url
end

function generate_data( x, y, zoom )
  local url = generate_url( x, y, zoom )

  local out = gondwana.make_u8vec4_tile()

  local is_load_cache_succeeded = out:load_cache( url );

  if not is_load_cache_succeeded then
    local std    = gondwana.make_u8vec4_tile_from_image_url( generate_sub( x, y, zoom, sub_key_std    ) )
    local relief = gondwana.make_u8vec4_tile_from_image_url( generate_sub( x, y, zoom, sub_key_relief ) )

    for n = 0, 256 * 256 - 1, 1 do
      local r0, g0, b0, a0 = std:at( n )
      local r1, g1, b1, a1 = relief:at( n )
      out:emplace_back
      ( math.floor( ( r0 / 255 ) * ( r1 / 255 ) * 255 )
      , math.floor( ( g0 / 255 ) * ( g1 / 255 ) * 255 )
      , math.floor( ( b0 / 255 ) * ( b1 / 255 ) * 255 )
      , math.floor( 255 )
      )
    end

    out:save_cache( url );
  end

  return out
end

それでは、上から順に解説していきましょう。

最初の4行はこのスクリプト全体で共通して使える「変数」を4つ、 url_pattern sub_pattern sub_key_std sub_key_relief と名前を付けて、中身は文字列で定義しています。

さて、 Lua 言語の単体のスクリプトは通常、上から順序にコードが実行されます。その点は GONDWANA のスクリプトでも同様ですが、 GONDWANA では Lua に特定の名前を付けた「関数」を用意する事で、 GONDWANA 側から必要に応じてスクリプトの中の機能を呼び出す事でソースデスクリプターの実行が進みます。次からそのような「関数」の定義が始まります。

最初に現れる関数は get_name です。この関数で return した文字列が GONDWANA が内部的にプログラムに組み込んで使う際に採用される「プログラム内での名前」になります。ここで使える名前にはプログラム内で動作させるためのルールがあり、単純には「アルファベット小文字とアンダースコアと数字だけで名前を付けてあげる必要がある」と認識して頂ければ問題が生じる事はありません。

次に現れる関数 get_appearance_name ではグラフィカルユーザーインターフェース上で「人間が見る表示用に用いられる名前」を return で与えます。

get_parameter_type_string では、 "x_y_zoom_type" を設定します。この値は GONDWANA がスクリプトに対して欲しいデータの情報を要求する際にスクリプト側へ与えるパラメーターに影響し、今回はGSIのタイルを元にするので x,y,zoom が与えられる設定を用います。他に lat, lon が与えられるパターンなどもありますが今回の解説では省略します。

get_data_type_string では "u8vec4_tile_type" を設定します。この値は GONDWANA へスクリプト側から最終的に渡すデータ形式がどのようなものかを決定します。u8vec4_tile_type は「unsigned な 8bit の要素 4 が並んだデータ」を意味します。アピアランスタイルでは通常ラスター形式の「画像」データを扱いますが、これはピクセルが並んだデータです。一般にラスター画像のピクセルは符号なしで 8bit の R, G, B, A の4つの要素で表すため、今回は "u8vec4_tile_type" となります。今回は他の形式についての解説は省略します。

zoom_min はこのタイルがネイティブに対応可能な最低のズームレベルを与えます。この値を下回るズームレベルはこのレベルの生成結果からGONDWANA側で自動的に合成(タイルズームレベルのオーバーロード機能)して生成されるようになります。今回は GSI-STD と GSI-RELIEF で共通する最小値 5 を設定します。

zoom_max はこのタイルがネイティブに対応可能な最高のズームレベルを与えます。この値を上回るズームレベルはこのレベルの生成結果からGONDWANA側で自動的に合成(タイルズームレベルのオーバーロード機能)して生成されるようになります。今回は GSI-STD と GSI-RELIEF で共通する最大値 15 を設定します。

ちなみに、 zoom_min, zoom_max は例えば 0-20 と設定しておいて Lua スクリプト内部で個別に要求されたズームレベルに応じてキャッシュやソース取得の制御を行う事もできますが、例としては複雑になるため今回は実用的にも十分な合成するソースに共通する設定を用いる事にしました。

次の is_heavy は通常は false で構いません。この値を true とすると「タイルのデータ生成は遅い」とGONDWANA側に認識させ、他のタイルの処理を優先的に行うようになります。データソースの取得が遅い場合や、合成処理が複雑で重い場合には true にすると他の、より高速に扱えるタイルの読み込み処理を妨害し難くなります。

generate_url は引数が3つあります。この引数のパターンは get_parameter_type_string の結果に応じて変化しますが、今回は get_parameter_type_string で "x_y_zoom_type" を与えていたので順に x, y, zoom が GONDWANA 側からスクリプトへ与えられて実行されます。この関数では通常はソースデスクリプターの取得元のURLを生成して return します。今回は合成となるため一元的なURLをスクリプト作成者が他のURLと被らないように注意して付けてあげます。

さて、ここで generate_url の内部の処理についても少しだけ解説します。先ず、 local はその行(厳密にはステートメント)で定義される「変数」がスクリプト全体ではなく、「スコープ」のみに束縛(ここでは限定と読み替えてもOK)される事を意味します。次に、 string.gsub は Lua 言語の組み込み関数の1つで、引数で与えられた「入力文字列、パターン、置換対象、回数」を用いて入力文字列中のパターンを置換対象で回数だけ置換した結果の文字列を取得する関数です。

次に generate_sub については generate_url に倣って定義していますが、今回この関数だけは他の関数と異なり、 GONDWANA のソースデスクリプターに必須のものではなく、スクリプト内部から便利でコード全体の見通しが良く使えるように定義した関数です。 generate_url ではソースデスクリプターとしての URL を生成しましたが、 generate_sub はこのスクリプトが内部的に扱う GSI-STD と GSI-RELIEF のそれぞれの URL を生成する補助的な内部用のURL生成関数として定義を用意します。

最後に、 generate_data 関数を定義します。この関数の定義内容がソースデスクリプターのもっとも重要な部分で、ここで GONDWANA へ今回であれば「アピアランスソース」として適切な画像データを生成して return します。この関数にも generate_url と同様の引数が与えられます。また、この関数で return するデータは先に get_data_type_string で与えたデータ型の種類に一致したデータを与える必要があります。今回は画像データによる u8vec4_tile_type のデータを return します。

先ず、 generate_url 関数を使い、ソースデスクリプターとしての URL を生成します。次に、 gondwana.make_u8vec4_tile 関数により、中身が空の u8vec4_tile_type の変数 out を用意します。そして、 out:load_cache( url ) として、 GONDWANA に url のキャッシュがあれば out へ読み出すよう定義します。

ここで、 gondwana.make_u8vec4_tile のように gondwana. で始まる関数は GONDWANA が独自の機能としてソースデスクリプターLua スクリプトへ与える GONDWANA の一部機能を呼び出すための関数です。また、 out:load_cache( url ) は u8vec4_tile_type のメンバー関数を呼び出す機能で、これも GONDWANA がソースデスクリプターへ与える固有の機能です。

out:load_cache を行うと2つの効果が発生します。1つは out へキャッシュデータが読みだし可能だった場合にはキャッシュから読みだしたデータが格納される効果です。もう1つはキャッシュデータの読み出しが成功したか失敗したかを示す true または false からなる boolean 値を関数呼び出しの結果として与える事です。今回はこの成否の結果を is_load_cache_succeeded 変数へ受け取っています。

次に、 if ... then ... end 制御構文により、条件分岐としてキャッシュの読み出しが行えなかった場合の処理を記述しています。

キャッシュを読み出せなかった場合には、 gondwana.make_u8vec4_tile_from_image_url 関数へ先に定義した generate_sub 関数を用いて生成した GSI-STD の URL を与えて、GSI-STD のタイルデータを取得、同様に GSI-RELIEF も取得、そして合成によって std と relief から out を生成するため for ... do ... end 制御構文により変数 n へ 0 から 256 * 256 (これは生成するタイル=画像のピクセル単位の横幅と縦幅に相当します)を与えて「ループ処理」を行います。

このループ処理の1回分の内容は、 std:at( n ) により、画像のピクセルの n 番目の R, G, B, A それぞれの値を取得、同様に relief についても取得し、 out:emplace_back へ合成結果の R, G, B, A 値を書き出すものです。ポイントは2つ。1つはここで扱うピクセルの各成分 R などの値は 0 から 255 までの符号なし整数のため、乗算合成処理を行う前に 0 .. 255 -> 0.0 -> 1.0 に値の範囲を変換し、「乗算」した後に 0.0 .. 1.0 -> 0.0 .. 255.0 へ範囲を戻す事。そしてもう1つ、 Lua では数値は意識せずに扱う場合、実数値として扱われますが、ここでの画像のピクセルの各成分としては整数値を設定したいため、 Lua に組み込みの math.floor 床関数で整数に整えてあげる事です。

キャッシュから読み出せすにSTD、RELIEFそれぞれのソース取得と合成処理を行った場合には、 for ... do ... end を終えて生成し終わった段階で out:save_cache( url ) として、次回同じタイルが必要になった際にはキャッシュから高速に読み出せるよう保存するようにします。

最後に return out として生成した画像の u8vec4_tile_type データを GONDWANA へ return して終わりです。

計算機のプログラムは人間が読む日本語の文字列で解説すると煩雑になってしまうものですが、もう一度改めてサンプルのソースコードを眺めて見て下さい。ソースコードはそれほど複雑には見えないでしょう。実際、この程度の単純な合成ソースであればどなたでもすぐに簡単に書きて使いこなせるようになるでしょう。

もう少し別の例が見たければ、 g3_altitude_contour.lua を眺めてみるのもお勧めです。データ形式の異なる標高形式のデータから画像形式データを生成するサンプルとしてソースデスクリプターの参考として役立つと思います。

なお、この記事で作成したサンプルの GSI-STD * GSI-RELIEF の合成ソースは次回のリリースで "G3.Experimental: std * relief" としてアピアランスソースに追加しておきますので、少しスクリプトを書いて自己流の合成ソースや何らかのデータから動的に生成したソースを用意してみようと考えている方は参考にどうぞ。