深度マップを地形のように表現してみた
深度マップを“地形”として立ち上げる実験です。分割数・水面・等高線の三点を追い込み、見た目と軽さの折り合いをつける。どこで手を打つか、数字で決めます。
WEBカメラの映像からAI推論で深度マップを作って、それを地形のように3D空間に表示するシステムを作った。
深度情報を、平面ではなく立体的に。山のような起伏、等高線、水面。地図のような表現にしてみた。
楽しい。
やりたかったこと
深度マップをただのグレースケール画像として表示するだけだと、つまらない。
深度情報って、地形と同じようなもの。高さがある。それなら、地図のように表現できるんじゃないか。
等高線を描いて、水面を作って、色分けをする。地形図のような美しさを出したかった。
地形表現の仕組み
深度マップを PlaneGeometry の各頂点の高さに反映させる。
// 立体表示用の高解像度ジオメトリ
const terrainGeometry = new THREE.PlaneGeometry(
160, // 幅
90, // 高さ
128, // 横方向の分割数
72 // 縦方向の分割数
);
128×72で分割してる。これで、深度マップの各ピクセルが、ジオメトリの頂点に対応する。
分割数は色々試した。
最初は64×36で試した。カクカク。地形の滑らかさが足りない。
256×144にしたら、滑らかになったけど、描画が重くなった。fpsが30以下に落ちた。
128×72が、品質とパフォーマンスのバランスが良かった。
ShaderMaterialで高さを表現
頂点シェーダーで、深度値を法線方向の移動量に変換する。
const terrainMaterial = new THREE.ShaderMaterial({
uniforms: {
depthTexture: { value: depthTexture },
displacementScale: { value: 35.0 }, // 凹凸の強さ
waterLevel: { value: 0.3 }, // 水面レベル
contourLines: { value: 15.0 }, // 等高線の数
colorMode: { value: 0 } // カラーモード
},
vertexShader: `
uniform sampler2D depthTexture;
uniform float displacementScale;
varying float vDepth;
void main() {
vUv = uv;
// 深度値を取得
vec4 depthColor = texture2D(depthTexture, uv);
float depth = (depthColor.r + depthColor.g + depthColor.b) / 3.0;
// 正規化(0〜1の範囲)
float normalizedDepth = depth;
vDepth = normalizedDepth;
// 法線方向に移動(高さを作る)
vec3 newPosition = position + normal * normalizedDepth * displacementScale;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
}
`,
fragmentShader: `...`,
side: THREE.DoubleSide
});
displacementScale は凹凸の強さ。35.0に設定してる。
最初は10.0で試した。凹凸が弱すぎる。地形の起伏が分からない。
50.0にしたら、凹凸が強すぎた。ポリゴンが荒く見えた。
35.0がちょうどいい。起伏がハッキリして、見やすい。
カラーモード(3種類の表現)
フラグメントシェーダーで、深度値に応じて色を付ける。3つのスタイルを実装した。
地形図風
深度を段階的に色分け。青(深い)→緑(中間)→黄色→赤(高い)。
if (vDepth < waterLevel) {
// 水面より下は青のグラデーション
float t = smoothstep(0.0, waterLevel, vDepth);
terrainColor = mix(colorMap1, colorMap2, t);
// 水面の波紋効果
float ripple = sin((vPosition.x * 0.5 + vPosition.y * 0.5) * 20.0 + time * 0.5) * 0.02;
terrainColor += ripple * vec3(0.1, 0.2, 0.3);
} else if (vDepth < 0.7) {
// 中間の高さは緑〜黄色
float t = smoothstep(waterLevel, 0.7, vDepth);
terrainColor = mix(colorMap2, colorMap3, t);
} else {
// 高い部分は黄色〜赤橙色
float t = smoothstep(0.7, 1.0, vDepth);
terrainColor = mix(colorMap3, colorMap4, t);
}
waterLevel は水面レベル。0.3に設定してる。
0.1だと水面が低すぎて、ほとんど陸地になった。
0.5だと水面が高すぎて、ほとんど水になった。
0.3がバランス良かった。
水彩画風
筆触効果とにじみを追加。水彩画のような柔らかい表現。
// 筆触効果のための関数
float brushPattern(vec2 uv, float scale) {
float pattern1 = noise(uv * scale + vec2(time * 0.1, time * -0.05));
float pattern2 = noise(uv * scale * 2.3 + vec2(-time * 0.12, time * 0.08));
float pattern3 = noise(uv * scale * 0.7 + vec2(time * -0.05, -time * 0.03));
return pattern1 * 0.5 + pattern2 * 0.3 + pattern3 * 0.2;
}
// 水彩画風のエフェクト
vec3 watercolorEffect(vec3 baseColor, float depth, float pattern) {
float edgeIntensity = abs(dFdx(depth)) + abs(dFdy(depth));
float bleed = pattern * 0.8;
vec3 bleedColor = mix(baseColor, shadowColor, bleed * 0.3);
vec3 edgeColor = mix(bleedColor, shadowColor, edgeIntensity * 3.0);
return edgeColor;
}
複数のノイズ層を重ねて、筆のストロークのような効果を作ってる。
抽象画風
明確な色分けとアクセント。現代アートのような表現。
vec3 abstractEffect(vec3 baseColor, float depth, float pattern) {
// 深度を5段階に分割
float sections = floor(depth * 5.0) / 5.0;
vec3 sectionColor = mix(colorMap1, colorMap3, sections);
// パターンで色を変化させる
float colorShift = pattern * 2.0 - 1.0;
sectionColor = adjustColors(sectionColor, 1.0 + colorShift * 0.3, 1.0 + colorShift * 0.5);
return sectionColor;
}
深度を5段階に分割して、ハッキリした色分けにしてる。
等高線の実装
深度を等間隔で区切って、線を描く。
// 等高線効果
float contourValue = fract(vDepth * contourLines);
float contourMask = 1.0 - smoothstep(0.0, contourWidth, abs(contourValue - 0.5));
// 等高線を追加
finalColor = mix(finalColor, shadowColor, contourMask * 0.5);
contourLines は等高線の数。15本に設定してる。
10本だと粗すぎて、高さが分かりにくかった。
20本だと細かすぎて、見づらくなった。
15本がちょうどいい。地形の起伏が分かりやすい。
ノイズ効果
FBM(Fractal Brownian Motion)で、より自然な地形にする。
// FBMによる自然な地形表現
float fbm(vec2 st) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 1.0;
// 4オクターブを重ねる
for (int i = 0; i < 4; i++) {
value += amplitude * noise(st * frequency);
st *= 2.1; // スケール変化
amplitude *= 0.5; // 振幅減衰
}
return value;
}
4オクターブで重ねてる。
最初は2オクターブで試した。地形が単調だった。
6オクターブにしたら、処理が重くなった。
4オクターブが、自然さとパフォーマンスのバランスが良かった。
dat.guiで調整可能に
パラメータをリアルタイムで調整できるUIを追加した。
- 地形の高さ(0〜100)
- ノイズ量(0〜0.1)
- 筆触効果(0〜2)
- 等高線の数(0〜50)
- 水面レベル(0〜1)
- カラーモード(地形図風/水彩画風/抽象画風)
GUIがあると、色々試せて面白い。パラメータの最適値を見つけるのに役立った。
ハマったところ
シェーダーの変数スコープ
最初、フラグメントシェーダーで noise 関数を定義せずに使ってた。エラーが出た。
// NG: noise関数を定義せずに使用
vec3 finalColor = baseColor + noise(vUv);
// OK: noise関数を先に定義
float noise(vec2 p) { ... }
vec3 finalColor = baseColor + noise(vUv);
GLSLは、関数を使う前に定義しないとエラーになる。JavaScriptと違う。
深度テクスチャの更新タイミング
深度推論は200msごとに実行してる。
最初は毎フレーム(約16ms)で実行してた。処理が追いつかなくて、画面がカクカクになった。
200msに変えたら、滑らかになった。深度マップが少し遅れるけど、許容範囲。
色のマッピング
最初、単純なグレースケール→色変換を試した。
// NG: 単純な変換
vec3 color = vec3(depth, depth * 0.5, 1.0 - depth);
これだと、色が単調だった。
Infernoカラーマップを使ったら、綺麗なグラデーションになった。科学的可視化で使われる配色なので、深度表現に向いてる。
パフォーマンス
PCでの動作を測定した。
| 処理 | 時間 |
|------|------|
| 深度推論(WebGPU) | 約20ms |
| ジオメトリ更新 | 約5ms |
| シェーダー描画 | 約10ms |
| 合計(1回の推論) | 約35ms |
推論間隔を200msにしてるので、表示は約5fpsで更新される。
アニメーション自体は60fpsで動くけど、深度データは5fpsで更新。見た目は滑らか。
WebGLだと深度推論が約35msかかる。WebGPUの方が速い。
スマホだと、深度推論が100ms以上かかった。実用的じゃない。PCでの利用を推奨。
結果
深度マップを地形のように表現できた。
- 地形図風、水彩画風、抽象画風の3スタイル
- 等高線で高さを表現(15本)
- 水面レベルで色分け(水面0.3)
- FBM(4オクターブ)で自然な起伏
- dat.guiでパラメータ調整
カラーモードを切り替えると、同じ深度データでも全く違う印象になる。面白い。
推論間隔を200msにして、パフォーマンスを確保した。リアルタイム性は少し犠牲になったけど、滑らかに動く。
地形を体験できるデモ:
全体のフローをまとめると:
これだ。
使ってみて
実際に体験してみてください:
デモを起動する(全画面表示)
ポイントは以下の3つ:
- 凹凸の強さは35.0が目安(強すぎるとポリゴンが荒れる)
- 等高線は15本が読みやすい(10本は粗、20本は細かすぎ)
- 水面レベル0.3で地形のコントラストが最も自然に出る
手応え。
まとめ
今回は、深度マップを地形として3D表示する実験を行いました。
ポイントは以下の4つ:
- PlaneGeometry(128×72分割)で深度を高さに変換
- ShaderMaterialで3つのカラーモード(地形図風/水彩画風/抽象画風)
- 等高線(15本)と水面(レベル0.3)で地形らしさを演出
- FBM(4オクターブ)でノイズを加えて自然な起伏
パラメータの調整が難しかったけど、dat.guiを使って色々試せるようにした。
深度情報の可視化方法を探している方の参考になれば嬉しいです。
さらに深く学ぶには
この記事で興味を持った方におすすめのリンク:
自分の関連記事:
最後まで読んでくださり、ありがとうございました。