属性データ駆動の色分けと3D建物グラデーション
第9章まででラスタータイルと3D Tilesを統合し、WebGISビューアの基本的な描画パイプラインが完成しました。しかし、これまでの描画は「固定色」が中心でした。GISアプリケーションでは、フィーチャーの属性データに応じて色を動的に変えたい場面が多々あります。建物の高さに応じた色分け、統計データによるヒートマップ、時刻による照明の変化など、データの意味を視覚的に伝えるスタイリングが重要です。
本章では、Three.jsのカスタムシェーダーを使い、属性データ駆動のスタイリングを実現する方法を学びます。GLSL ESの基本概念から、実践的なchoropleth(階級区分図)や高さグラデーション、リアルタイムパラメータ更新まで、段階的に解説します。
以下のコントロールで3D建物のシェーダーをリアルタイムに変更できます。パラメータを変更したら「シェーダーを適用」ボタンを押してください。
Three.jsには3つのレベルでシェーダーをカスタマイズする方法があります。
MeshBasicMaterial、MeshStandardMaterialなど。色、テクスチャ、透明度などをプロパティで制御できますが、描画ロジック自体は変更できません。現在のMVTレンダラー(mvt-mesh.ts)はこのレベルを使っています。
組み込みマテリアルの内部シェーダーを文字列置換で改変する方法です。既存のライティング計算やシャドウマップなどの機能を維持したまま、一部のロジックだけを差し替えられます。3D Tilesのように、ライブラリが生成するメッシュのマテリアルを後から改変する場合に有効です。
頂点シェーダーとフラグメントシェーダーを完全に自前で書く方法です。完全な制御が可能ですが、Three.jsの組み込み機能(ライティング、シャドウなど)は自分で実装する必要があります。MVTポリゴンのように単純な塗りつぶしで十分な場合に適しています。
WebGLで使用するGLSL ES(OpenGL ES Shading Language)の基本概念です。
| 修飾子 | 方向 | 用途 |
|---|---|---|
attribute | CPU → 頂点 | 頂点ごとのデータ(位置、色、属性値) |
uniform | CPU → シェーダー | 全頂点共通(行列、時刻、色パラメータ) |
varying | 頂点 → フラグメント | 頂点間で補間されるデータ |
Three.jsが自動的に注入するuniform・attribute:
// 頂点シェーダーで自動的に使える変数
uniform mat4 projectionMatrix; // カメラの射影行列
uniform mat4 modelViewMatrix; // モデル×ビュー行列
uniform mat4 modelMatrix; // モデル行列
attribute vec3 position; // 頂点位置
attribute vec3 normal; // 法線ベクトル
attribute vec2 uv; // テクスチャ座標PLATEAUのLOD1建物データは、各建物がボックス型のメッシュです。ECEF座標系での頂点位置から地球中心までの距離を計算し、WGS84楕円体の半径(約6,378,137m)を引くことで概算の建物高さが得られます。
onModelLoadedコールバックで生成されたメッシュにアクセスし、onBeforeCompileで既存マテリアルのシェーダーを改変します。
// フラグメントシェーダーで高さに応じた色を計算
float h = length(vWorldPos) - 6378137.0;
float t = clamp(
(h - uMinHeight) / (uMaxHeight - uMinHeight),
0.0, 1.0
);
vec3 color = mix(uColorLow, uColorHigh, t);length(vWorldPos)はECEF原点(地球中心)からの距離です。地表面の半径を引けば概算の建物高さが得られます。厳密な計算にはWGS84楕円体の偏平率を考慮する必要がありますが、色分け程度の精度では十分です。
Three.jsの組み込みシェーダーは#include <chunk_name>で構成されています。onBeforeCompileで文字列置換を行い、独自ロジックを挿入します。
material.onBeforeCompile = (shader) => {
// uniformを追加
shader.uniforms.uMinHeight = { value: 0 };
shader.uniforms.uMaxHeight = { value: 200 };
// 頂点シェーダーにvarying追加
shader.vertexShader = shader.vertexShader
.replace(
'#include <common>',
`#include <common>
varying vec3 vWorldPos;`
);
// フラグメントで高さ色混合
shader.fragmentShader = shader.fragmentShader
.replace(
'#include <color_fragment>',
`#include <color_fragment>
diffuseColor.rgb = mix(...);`
);
};代表的なシェーダーチャンク:
#include <common> — 定数と基本関数の定義#include <color_fragment> — 拡散色の決定#include <worldpos_vertex> — ワールド座標の計算#include <lights_fragment_begin> — ライティング計算の開始太陽位置をuniformで渡し、時刻に応じた照明の変化をシミュレートします。太陽赤緯(季節変動)と時角から方向ベクトルを概算します。
function computeSunDirection(
hourUTC: number,
dayOfYear: number
): THREE.Vector3 {
// 太陽赤緯(季節変動)
const decl = 23.44 * Math.sin(
2*PI/365 * (dayOfYear - 81)
);
// 時角
const ha = (hourUTC - 12) * 15;
// ECEF → シーン座標変換
return new Vector3(x, z, -y);
}フラグメントシェーダーではランバート反射モデル(max(dot(normal, sunDir), 0.0))を使い、法線と太陽方向の内積から拡散光の強さを計算します。環境光(ambient)を加えることで、影になる面でも真っ黒にならないようにしています。
| 操作 | コスト | 再コンパイル |
|---|---|---|
| uniform値の変更 | 非常に低い | 不要 |
| attribute値の変更 | 中程度 | 不要 |
| attributeの追加/削除 | 高い | 不要 |
| シェーダーコード変更 | 非常に高い | 必要 |
設計原則: 動的に変化するパラメータ(色のレンジ、不透明度、時刻など)はuniformで渡し、フィーチャー固有のデータ(属性値)はattributeで渡す。シェーダーコードの変更はアプリケーション起動時に行い、ランタイムでは避ける。
上のコントロールパネルで時刻スライダーを動かす操作は、uniformの値を書き換えるだけなので、シェーダーの再コンパイルは発生しません。一方、シェーダーモードの切り替え(高さグラデーション → フラットカラー等)はシェーダーコード自体が変わるため、レイヤーの再構築が必要です。
多数の色クラスを使う場合、シェーダー内でif-else分岐を増やすとGPUのワープ分岐ペナルティが発生します。代わりに、1Dテクスチャをカラーパレットとして使うと分岐なしで高速にルックアップできます。
// CPU側: パレットテクスチャ生成
const data = new Uint8Array(width * 4);
for (let i = 0; i < width; i++) {
data[i*4+0] = colors[i].r * 255;
data[i*4+1] = colors[i].g * 255;
data[i*4+2] = colors[i].b * 255;
data[i*4+3] = 255;
}
// GPU側: テクスチャルックアップ
float idx = floor(
vValue * (uPaletteSize - 1.0)
) + 0.5;
vec2 uv = vec2(
idx / uPaletteSize, 0.5
);
vec4 c = texture2D(uPalette, uv);既存のmvt-mesh.tsはマテリアルプーリングで描画コールを最適化しています。カスタムシェーダーを導入する場合も同じ戦略を適用します。
choroplethでは頂点カラー(attribute)で各フィーチャーを区別するため、マテリアルの共有がさらに容易です。1つのShaderMaterialを全ポリゴンで共有し、色の違いはattributeで表現する設計により、ドローコールを最小限に抑えられます。
const pool = new Map<string, ShaderMaterial>();
function createChoroplethMaterial(
opacity = 0.7
): ShaderMaterial {
const key = `choropleth-${opacity}`;
let mat = pool.get(key);
if (!mat) {
mat = new ShaderMaterial({...});
pool.set(key, mat);
}
return mat;
}第9章で実装した独自の3D Tilesアダプターがタイルをロードすると、Tiles3DAdapterCallbacksのonModelLoadedコールバックで生成されたメッシュにアクセスできます。onModelDisposedと合わせて、マテリアル差し替えとクリーンアップを行います。
interface Tiles3DAdapterCallbacks {
onTilesetLoaded?: (sphere) => void;
onLoadError?: (error, url) => void;
onModelLoaded?: (
scene: Object3D, tile: object
) => void;
onModelDisposed?: (
scene: Object3D
) => void;
}Tiles3DLayerのshaderModeプロパティに応じて、onModelLoadedで適切なシェーダー適用関数(applyHeightGradient、applyFlatColor、applySunLighting)を呼び出します。onModelDisposedではdisposeCustomMaterialsでカスタムマテリアルをクリーンアップします。
本章では3つのレベルでカスタムシェーダーを導入する方法を学びました:
これらの技法を組み合わせることで、データの意味を視覚的に伝えるGISビジュアライゼーションが可能になります。次章では、全体の統合と今後の拡張について議論します。