以前、BlenderのGeometry Nodesでプロシージャルな街を自動生成してみたという記事を書きました。グリッドに点を並べ、いらない点を消して、残った点に箱を置く。たったこれだけのルールで、1,600区画の街が生まれる回です。

あの記事を書いたあと、保存しておいたファイルを開き直して気づきました。あの街には、道路がありません。

正確に言うと、道路に見えていたものは「点を消した空白」でした。ビルを置かない列を作れば、そこは通り抜けられる隙間になる。それらしく見えるので前回はそれで通しましたが、アスファルトも白線も、実体は何ひとつ置いていなかったのです😅

そこで今回は、あの街に道路の実体・地面の高低差・建物の作り分けを段階的に足していきます。ただ足すだけなら「もっとリッチになりました」で終わってしまうので、もうひとつ軸を持たせました。

作り込むほど重くなる。その「重さ」には2種類ある。

  • (a) ノードの再計算… スライダーを動かしたときに待たされる時間(CPU側の仕事)
  • (b) ビューポートの描画… 視点をぐるぐる回したときの滑らかさ(GPU側の仕事)

この2つは別物で、何を足したかによって伸びる側が変わります。片方だけ見ていると、対策を打つ場所を間違えます。

結論を先に書くと、いちばん意外だったのはここでした。

  • 画面に出る面の数が4割減ったのに、描画時間は2.3倍に増えた
  • Raycastノードの実行時間は0.1ミリ秒未満。重さは隣のノードに計上されていた
  • インスタンスを実体化すると、描画は10倍速くなり、再計算は3.6倍遅くなった

前回の街には、道路がありませんでした

スポンサーリンク

まず出発点を確認します。前回のノード構成はこうでした。

グリッド(41×41の点)
   ↓
メッシュのポイント化(各頂点を「点」にする)
   ↓
ジオメトリ削除(6区画ごとの行・列=道路 + ランダムに約1割=空き地)
   ↓
ポイントにインスタンス作成(残った点に箱を1つずつ)

高さは点の番号から作った擬似乱数で決めています。前回と同じ式をそのまま使いました。

乱数     = 小数部分( sin(点の番号 × 12.9898) × 43758.5453 )   … 0〜1のバラバラな値
ビルの高さ = 1 + 乱数 × 4                                      … 1〜5m

この状態を、今回は①前回のままと呼びます。寄って見るとこうです。

道路が空白のままの街。区画の隙間に地面がのぞいている

①前回のまま。道路に見えている部分は、ビルを置かなかっただけの空白です

前回から1つだけ変えたこと

前回の街は1区画が1mでした。今回それを2mに広げています。1mのままだと建物が細長い柱のようになり、道路を敷いても俯瞰でほとんど見えなかったためです。道路も2区画ぶんの幅にし、8区画ごとに通しました。この記事の①〜⑦はすべて同じ設定で揃えています。

用語集

今回は聞き慣れない言葉がいくつか出てきます。先にまとめておきます。

用語平たく言うとなぜ効くのか
インスタンス同じ形の「分身」。データは1つだけ持ち、置き場所だけをたくさん記録する1,000棟置いてもデータはほぼ1棟ぶん。前回「重くならない」と書いた理由がこれ
Realize Instances(実体化)分身をやめて、全部を本物のメッシュに焼き固める分身のままでは使えない操作(頂点単位の加工など)が使えるようになる。代わりにデータが本当に増える
フィールド「点ごとに違う値」を計算する仕組み。値そのものではなく計算の予定が線を流れる★今回の山場。計算は受け取った側で実行されるので、重いノードと重く見えるノードが一致しない
Raycast指定した向きに光線を飛ばし、何かに当たった場所・傾きを調べる「この点の真下の地面は、どの高さでどっちに傾いているか」を後から知ることができる
デプスグラフBlenderが「何を作り直すべきか」を管理している依存関係の表スライダーを1つ動かすと、ここを通って再計算が走る。(a)の待ち時間の正体
ビューポート作業中の3Dビュー(レンダリング前の表示)(b)の描画時間はここの話。最終レンダリングの時間とは別物
LOD(間引き)遠くのものを簡略化したり消したりして軽くする定番の手法今回は「編集中だけ建物を減らし、レンダリングでは元に戻す」形で使う

今回の検証環境

3DCGの実測はWindows機(RTX 3060)で行い、記事はMacで書いています。ノードの組み立てからレンダリングまで、数字と画はすべてWindows側で出したものです。

項目内容
OSWindows 11 Pro(10.0.26200)
CPU / RAMAMD Ryzen 7 7840HS / 31.3 GB
GPUNVIDIA GeForce RTX 3060 12GB(ドライバ 610.47)
Blender5.2.0 LTS(zip版・既存のインストールとは別に展開)
画面1920×1080 / 60Hz
街の大きさ1区画2m。グリッド41/81/161/241の4段階(241=480m四方

測り方はこうしました。

  • (a) 再計算… グリッドの大きさをごくわずかに動かして街を作り直させ、その1回にかかる時間を測る(7回測って中央値)
  • (b) 描画… 視点を少しずつ回しながら、ビューポートを実際に描き直させて1コマぶんの時間を測る。形は変えていないので再計算は走らず、描画だけが出てくる

(b)にはBlender自身のベンチ用オペレータ wm.redraw_timer を使いました。ここで1つ落とし穴を踏んでいます。

DRAW_WIN_SWAP で測ると、画面のリフレッシュレートで頭打ちになります

このモードは「描いて画面に出す」ところまで含むため、垂直同期に引っかかります。120Hzの画面では8.3ミリ秒より速い値が絶対に出ません。実際、最初の下見では全段階が判で押したように8.3msに並び、差がまったく見えませんでした。描画そのものだけを測りたいときは、画面表示を含まない DRAW を使います。

①道路を「消す」から「置く」へ

まず道路に実体を与えます。やることは単純で、これまで消していた点を、今度は残して使うだけです。

点の列番号 ix = 点の番号 % グリッドの一辺
点の行番号 iy = floor(点の番号 ÷ グリッドの一辺)

縦の道路か = (ix % 8) < 2      … 8区画ごとに2区画ぶんの幅
横の道路か = (iy % 8) < 2
道路か     = max(縦の道路か, 横の道路か)

建物側 : 「道路か」が1の点を削除 → 残りに建物を置く
道路側 : 「道路か」が0の点を削除 → 残りに路面タイルを置く

同じ判定を、建物側では「消す条件」、道路側では「残す条件」として使い回しています。1つの判定から2つの枝が伸びる形です。

道路の実体を作るノード群。路面タイルと中央線の枝が分かれている

道路の実体を作る部分。路面タイル(アスファルト)と中央線が別の枝になっています

中央線には、ひとつだけ条件を足しました。交差点には線を引かないという条件です。縦の道路と横の道路が両方成立している点=交差点なので、そこだけ線の枝から外します。

交差点か   = 縦の道路か × 横の道路か        … 両方1のときだけ1
線を引くか = 1 − 交差点か

線の向きも、縦の道路のときだけ90度回します。縦の道路か(0か1)にそのまま90度を掛ければ、条件分岐を書かずに向きが決まります。

アスファルトと黄色い中央線が敷かれた街。交差点には線がない

②道路を置く。交差点では線が途切れています

見た目は素直に良くなりました。ところが数字のほうは、この時点ですでに妙なことになっています。それは後半でまとめて見ます。

②地面に起伏をつける──どこに入れるかで結果が変わる

次は平らな地面に起伏をつけます。ノイズテクスチャで高さを作り、Set Position で地面をZ方向に押し上げるだけ……なのですが、ノードツリーのどこに入れるかで結果が変わります

今回の構成では、グリッドから点を作り、そこから建物と道路の2本に枝分かれしています。起伏を枝分かれの前に入れれば、建物も道路も同じ高さの上に乗ります。逆に枝分かれの後で片方にだけ入れると、もう片方が平らなまま取り残されます。

【正しい】 グリッド → 起伏 → 点にする →┬→ 建物
                                        └→ 道路

【まずい】 グリッド → 点にする →┬→ 起伏 → 建物
                                 └→        道路(平らなまま)

この「まずい」ほうを、eightは実際にやってしまいました。詳しくは次の章で書きます😳

起伏のついた地面の上に建物と道路が乗っている

③起伏(分岐の前)。路面タイルは水平の板のまま、階段状に斜面を登っていきます

よく見ると、路面タイルは1枚1枚が水平のままです。地面の高さには追従していますが、傾きには合っていません。斜面では板が階段状に並びます。ここを直すには「その点の地面がどっちに傾いているか」を知る必要があり、それが次のRaycastです。

③Raycastで地面に載せる(と、道路だけ取り残されました)

もうひとつのやり方がRaycastです。点はいったん平らなまま進めておき、各点の100m上から真下に光線を飛ばして、起伏した地面に当たった位置と傾きを拾います。

発射位置   = その点の位置 + (0, 0, 100)
向き       = (0, 0, −1)
長さ       = 300m

当たった位置 → Set Position で点をそこに置く
当たった法線 → Align Rotation to Vector で路面タイルを傾ける

回りくどく見えますが、この方式には利点があります。地面が別のオブジェクトでも、ノイズではなく実測の地形データでも、同じノードで載せられることです。「高さの計算式」を知らなくても地面に沿わせられます。

Raycastで地面の高さと傾きを拾うノード群

④起伏(Raycast)。当たった位置で点を置き直し、当たった法線で路面タイルを傾けています

⚠ つまずき:道路だけが地面に埋まりました

最初の版では、この Set Position建物と道路に枝分かれした後に置いていました。その結果、建物はきちんと起伏に乗るのに、道路の点だけが高さ0のまま取り残されます。地面が高いところでは路面が土に埋まり、低いところでは宙に浮き、土と路面がぶつかる場所ではモアレのようなちらつき(Zファイティング)が出ました。

厄介だったのは、建物が正しく乗っているので数字は普通に出ていたことです。計測値だけ見ていれば異常に見えません。画で並べて初めて分かりました。数字が出ているからといって、意図した形になっているとは限らないという、毎回どこかで踏む種類の失敗です。

直し方は簡単で、Set Position を枝分かれの前に移すだけでした。ただしRaycastを飛ばす点が「間引いたあとの建物」から「全区画」に増えるので、再計算時間は素直に増えます(480m四方で18.45ms→23.96ms、約30%増)。この記事の数字はすべて修正後のものです。

Raycastで地面に載せた街。路面が地面の傾きに沿っている

修正後。路面タイルが地面の傾きに沿い、斜面でも板が浮かなくなりました

★Raycastは「無料」に見える

ここが今回いちばん面白かったところです。

この④は、③(同じ見た目を作る分岐前の方式)に比べて再計算が3.9倍になりました。犯人はどう考えてもRaycastです。そこでBlenderのノードエディタでノードごとの実行時間を表示させて、どのノードが食っているのかを見にいきました。

ノードごとの実行時間。Raycastは0.1ms未満、Set Positionが15ms

★「真下へ光線を飛ばす」=0.1ms未満。「当たった高さに置く」=15ms

ノード実行時間
真下へ光線を飛ばす(Raycast 本体)< 0.1 ms
当たった高さに置く(Set Position)15 ms
路面を敷く(Instance on Points)5.8 ms
道路の点だけ残す(Delete Geometry)0.44 ms
路面タイル/わずかに浮かす/アスファルト< 0.1 ms

Raycastは、ほぼゼロと表示されます。

これはバグでも計測ミスでもなく、Geometry Nodesの仕組みそのものです。用語集に書いたフィールドがここで効いてきます。

フィールドは「値」ではなく「計算の予定」

Raycastノードから出ている線を流れているのは、計算済みの座標ではありません。「点ごとに、こう計算してください」という予定です。実際に何万回も計算が走るのは、それを受け取って点に適用する側──つまり Set Position の中です。だから時間はそちらに計上されます。

言い換えると、ノードの実行時間は「そのノードが何をするか」ではなく「そのノードが何回計算を回すか」で決まります。重い処理を書いたノードではなく、それを大量のデータに当てているノードを見にいくことになります。

この見方を知らないと、「Set Positionが重いらしい」という誤った結論に行き着きます。実際には位置を書き込む処理が重いのではなく、そこに刺さっているフィールドが重いのです。対策も変わってきます。Set Positionをどうにかしても速くなりませんが、Raycastを掛ける点の数を減らせば効きます。

⚠ フレームの合計=段階全体の時間、ではありません

このフレーム内の表示合計は15msですが、同じ条件の全体の再計算は23.96msでした。「路面を敷く」の5.8msが別枠に計上されているなど、内訳の帰属は素直ではありません。オーバーレイは「どのノードが重いか」の当たりをつける道具と考えて、総量は別に測るのが安全です。

④建物の作り分けと、中心ほど高いゾーニング

ここまで建物はすべて同じ箱でした。4種類に増やします。

  • A… 素の箱(前回と同じ)
  • B… 段違い(下が太く、上が細い)
  • C… 屋上に設備が乗った箱
  • D… 低層で敷地いっぱいに広い建物

この4つを1つのコレクションに入れ、Collection InfoSeparate Children で1つずつ取り出せる状態にします。あとは Instance on PointsPick Instance を有効にして、Instance Index に0〜3の乱数を流すだけです。

種類番号 = floor( 乱数 × 4 )      … 0〜3
Instance on Points:
    Pick Instance  = ON
    Instance Index = 種類番号

さらに「中心ほど高い」というゾーニングを足しました。街の中心からの距離を測り、近いほど大きい倍率を高さに掛けます。

距離     = √(x² + y²)
正規化   = 1 − 距離 ÷ (街の半径)        … 中心で1、外周で0
倍率     = max(正規化, 0)² × 3 + 1      … 中心で4倍、外周で1倍
最終の高さ = 元の高さ × 倍率

2乗しているのは、中心付近だけを急に高くして、都心と郊外の差をはっきりさせるためです。

屋上設備・段違い・低層ワイドの建物が混在した街並み

⑤建物4種+ゾーニング。屋上に設備が乗った棟、途中で細くなる棟、低く横に広い棟が混ざっています

ここは画角と明るさで失敗しました。最初に用意したカットは街全体を俯瞰する暗いライティングだったため、4種類あるはずの建物が全部シルエットになり、まったく描き分けが伝わりませんでした。寄りかつ明るい専用カットを別に用意し直しています。「数字は出たが、見た目で分からない」も立派な失敗だと思い知りました。

そして、この⑤は再計算がほとんど増えません(③6.19ms → ⑤5.43ms)。計算式が増えただけで、扱うデータの量は変わっていないからです。むしろわずかに下がっていますが、これは計測のばらつきの範囲とみています。

★計測:重さは2種類あって、伸び方が違う

ここからが本題です。480m四方(グリッド241)の街で、①〜⑦を並べます。

作り込むほど、どちらが重くなったか(480m四方の街) 上=ノードの再計算(作り直しの待ち時間) / 下=ビューポート描画(ソリッド表示・1コマ)。単位はミリ秒 0 5 10 16 21 26 ①前回のまま 2.0 4.8 ②道路を置く 2.9 11.1 ③起伏(分岐の前) 6.2 11.0 ④起伏(Raycast) 24.0 11.0 ⑤建物4種+ゾーニング 5.4 12.3 ⑥編集中だけ間引く 5.5 7.7 ⑦実体化 19.5 1.2 ノードの再計算 ビューポート描画

形がまったく違います。

  • ④(Raycast)は再計算だけが飛び抜けて、描画は③とほぼ同じ。作るのが遅いだけで、出来上がったものは同じ重さ
  • ⑦(実体化)は正反対。描画は1.2msまで落ちるのに、再計算は19.5msまで跳ね上がる
  • ⑥(間引き)は描画だけを下げて、再計算はまったく変えていない

もし「重い」を一言でしか測っていなかったら、④と⑦は「どちらも約20msで同じくらい重い」という結論になっていたはずです。実際には、④はパラメータをいじるたびに待たされる街で、⑦は視点を回すのが快適な街です。作業の何が辛いかが正反対なのに、1つの数字にすると同じに見えてしまいます。

街を広げたときの伸び方も見ておきます。

街を広げると、再計算はどう伸びるか 縦軸=ノードの再計算(ミリ秒)。横軸=グリッドの一辺(頂点数)。区画数はこの2乗が目安 0 5 10 16 21 26 41 81 161 241 ④起伏(Raycast) ⑦実体化 ③起伏(分岐の前) ⑤建物4種 ①前回のまま

①③⑤は、街を36倍(区画数で)に広げても数ミリ秒しか増えません。一方で④と⑦は右肩上がりに伸びていきます。「今は軽いから大丈夫」が、規模を上げた瞬間に崩れる組み方かどうかは、この形を見ると判断できます。

★面数ではなく、インスタンスの数で決まっていた

ここで、①→②(道路を置いた)の描画時間に戻ります。2.3倍に増えていました。当然「面が増えたからだろう」と思ったのですが、面の数を数えてみると逆に減っていました

面数が減ったのに、描画は2.3倍になった ①前回のまま → ②道路を置く(480m四方)。道路タイルは1枚1ポリゴンなので面数は減っている 画面に出る面の数 232,530 136,409 ▼ 0.59倍 分身(インスタンス)の数 29,155 78,809 ▲ 2.70倍 ビューポート描画(ms) 4.79 11.07 ▲ 2.31倍 上段=①前回のまま 下段=②道路を置く

種を明かすと、道路の路面タイルは1枚が1ポリゴンの板です。一方、消えた建物は1棟6ポリゴンの箱です。道路の列から建物が消え、そこに板が入ったので、面の総数は4割減りました。それでも描画時間は2.3倍になっています。

伸び方が一致しているのはインスタンスの数のほうでした(2.70倍に対して描画2.31倍)。GPUに「これを描いて」と指示を出す回数がそのまま効いている、ということです。1枚の板であっても、別々に置かれていれば別々に指示が要ります。

この見方で読むと、⑦実体化が描画1.2msまで落ちるのも腑に落ちます。実体化すると41万ポリゴンの巨大な1つのメッシュになり、面の数は⑤とまったく同じなのに、指示は1回で済むからです。

まとめると

  • 再計算(作るとき)は、扱う点の数と、点ごとに回す計算の重さで決まる
  • 描画(見るとき)は、面の数よりもバラバラに置かれた物体の数で決まる

だから「軽くしたい」と思ったとき、まずどちらが辛いのかを決めてから手を選ぶ必要があります。

軽くする3つの手

手1:編集中だけ間引く

Geometry Nodesには Is Viewport というノードがあります。いま作業中のビューポートなら1、最終レンダリング中なら0を返すだけの、とても単純なノードです。これを削除条件に掛け合わせると、編集中だけ建物を減らし、レンダリングでは元に戻すという切り替えが作れます。

消すか = (中心からの距離 > しきい値) × Is Viewport

→ 編集中     : 遠い建物が消える(軽い)
→ レンダリング: Is Viewport が0なので何も消えない(元通り)

今回は「街の中心から120mより外」を条件にしました。カメラ基準ではなく街の中心基準なので、視点を動かしても消える範囲は変わりません。ビューポートではこう見えます。

間引きなしのビューポート。建物が全面に並んでいる

⑤間引きなし。オブジェクト数 78,812

間引きありのビューポート。中心の円の中だけ建物が残っている

⑥間引きあり。中心の円の中だけ建物が残ります。オブジェクト数 55,342

建物は減っていますが、道路と地面はそのまま残しているので、街の骨格は見えたままです。効果は描画 12.3ms → 7.7ms(−37%)。そして再計算は 5.4ms → 5.5ms で変わりません。消すかどうかを判断する計算自体は、消す前の全部の点に対して走るからです。

手2:表示方法を「バウンズ」にする

ノードを一切触らずに効く手もあります。オブジェクトの表示方法をバウンズ(外接する箱だけを線で描く)に変えるやり方です。

バウンズ表示にしたビューポート。箱の輪郭だけが描かれている

形は分からなくなりますが、位置と占有範囲は分かります

形が見えなくなるので常用はできませんが、「配置だけ確認したい」「別のオブジェクトを作り込みたいので、街には黙っていてほしい」というときには効きます。480m四方だと線が密集して真っ黒な塊になってしまったので、上の画像は160m四方のものです。規模によっては、この手自体が使い物にならないこともあります😅

手3:実体化する(ただし引き換えがあります)

最後が Realize Instances です。分身をやめて全部を本物のメッシュに焼き固めます。

項目⑤インスタンスのまま⑦実体化
ビューポート描画12.26 ms1.21 ms10.1倍速い
ノードの再計算5.43 ms19.54 ms3.6倍遅い
画面に出る面の数413,698413,698同じ
見た目実質同じ(レンダリング画像を画素単位で比べても差は誤差の範囲)

eightは実体化を「重くなるもの」だと思い込んでいました。実際には再計算を犠牲にして描画を買う操作でした。

使いどころは、はっきり分かれます。

  • 形をもう触らない/視点を回して確認したい → 実体化が効く
  • パラメータをまだ探っている最中 → 実体化するとスライダーのたびに待たされる

前回の記事で「プロシージャルの醍醐味はスライダーを動かすと街が激変すること」と書きましたが、その醍醐味を味わっている最中は、実体化してはいけないということになります。

予想の答え合わせ

いつもどおり、測る前に立てた予想を並べておきます。

予想結果実際
いちばん重いのは高低差のRaycast③6.19ms → ④23.96ms。全グリッドで単独首位
実体化すると重くなる再計算だけ当たり。描画は逆に10倍速くなった
間引きは描画には効くが、再計算には効かない描画 −37%、再計算 5.43→5.49msで変化なし
道路を実体化すると分身が増えて描画が伸びる分身2.70倍に対し描画2.31倍。ただし面数は減っていた
ゾーニングは計算式が増えるだけなので、再計算はあまり伸びない6.19 → 5.43ms。むしろ微減

4つ当たって1つが半分外れ、という結果でした。外れた1つ(実体化)が、いちばん考え方を書き換えられた項目でもあります。

まとめ

  • ✅ 前回の街に道路の実体・地面の高低差・建物4種の作り分けを足し、480m四方・78,809個の分身の街まで広げた
  • ✅ 「重い」をノードの再計算ビューポート描画の2つに分けて測った。両者は伸び方がまったく違い、1つの数字にすると正反対の性質が同じに見える
  • Raycastノードの実行時間は0.1ms未満。重さは受け取った側(Set Position)に15msとして計上されていた。フィールドは受け取った側で計算されるため
  • 描画の重さは面数ではなくインスタンスの数で決まっていた。面数が0.59倍に減っても、分身が2.70倍になれば描画は2.31倍になる
  • 実体化は「重くなる操作」ではなく「再計算を犠牲に描画を買う操作」。描画10.1倍速・再計算3.6倍遅。形をまだ探っている間は使えない
  • ⚠ つまずき:起伏を枝分かれの後に入れたため、道路だけが平らなまま取り残された。建物は正しく乗るので数字には出ず、画で並べて初めて分かった
  • ⚠ つまずき:描画の計測で画面表示を含むモードを使うと垂直同期で頭打ちになり、全段階が同じ値に並んだ

前回は「ノードを組めば街が生える」ことに驚いた回でした。今回はその先で、組み方によって、辛くなる場所が変わることが分かった回になりました。同じ見た目の街でも、作るのが遅い街と、見るのが遅い街があります。どちらを引き受けるかは、ノードのどこに何を置くかで自分が決めていた、というのが今回いちばんの収穫でした😊

それでは、今回はここまで。最後までありがとうございました😊

※ 計測はWindows機(Ryzen 7 7840HS / RTX 3060 12GB / Blender 5.2.0 LTS / 1920×1080 60Hz)で実施しました。再計算は7回測った中央値、描画は視点を回しながら12箇所で測った中央値です。描画時間はソリッド表示の値で、マテリアルプレビュー表示ではおおむね3〜4倍になりますが、段階ごとの順位は変わりません。数値はBlenderの版・ドライバ・シーンの内容によって変わります。★同じ傾向(Raycastの突出、実体化で描画が速くなること)はMac(M4)でも確認しています。