Depth ControlNet + Blender で空間ごと制御する — 3D部屋からデプスマップ生成

Depth ControlNet + Blender で空間ごと制御する — 3D部屋からデプスマップ生成 — Depth ControlNet, Blender, 3D 部屋 AI画像

こんにちは、パレイド技術部です。

前回は ControlNet Canny + LoRA で背景画像のエッジ構造を維持しつつ生成する方法を試しました。

Canny は輪郭線を保持してくれますが、あくまで 2D のエッジ情報 であり、奥行きやパースペクティブは制御できませんでした。モニターが歪む、壁との距離感がおかしい、カメラアングルが安定しない――こうした問題は Canny だけでは根本的に解決できません

今回は Depth ControlNet + Blender という組み合わせで、3D 空間そのものを制御する方法に切り替えます。結論から言うと、この方法で安定した生成がようやく実現しました。

なぜ Depth なのか

Canny エッジマップが持つ情報は「ここに輪郭線がある」という 2D の事実だけです。モニターの四辺もデスクの端も壁の境界も、すべて同じ「白い線」として扱われます。手前のモニターと奥の壁が同じ太さのエッジになるため、拡散モデルにとっては近い・遠いの区別がつきません。

Cannyの例。2Dの線画で表現

一方、デプスマップは 空間的な関係 をエンコードします。近いオブジェクトは白く、遠いオブジェクトは黒く描かれます。拡散モデルは「ここは手前のデスク」「ここは奥の壁」という立体構造を理解できるようになります。

さらに Blender との相性 が良く、カメラの位置や角度を自由に調整できます。3D モデルを一度作れば、カメラを動かすだけで何パターンでもデプスマップを生成できます。

3D深度の例。

Python で 3D 部屋を構築する

3D といえば Blender の出番です。今回の生成にもBlenderを利用しています。

3Dシーンの構築には Blender を使用しています。Blender は無料で利用できるオープンソースの3DCGソフトで、モデリング・レンダリング・アニメーションまで一通り対応しています。

といっても、手動で 3D モデリングする必要はありませんtrimesh を使えば Python コードでジオメトリを構築できます。部屋のレイアウトは RoomConfig データクラスのパラメータで定義します。

@dataclass
class RoomConfig:
    """部屋のレイアウト設定(メートル単位)。"""
    room_width: float = 4.0        # 部屋サイズ
    room_depth: float = 3.5
    room_height: float = 2.8
    desk_width: float = 1.6        # デスク
    desk_height: float = 0.75
    monitor_width: float = 0.60    # デスクトップモニター
    monitor_height: float = 0.35
    tv_width: float = 1.60         # 壁掛けテレビ
    tv_height: float = 0.90
    tv_center_y: float = 2.0
    cam_y: float = 1.4             # カメラ(目の高さ)
    cam_z: float = 1.5
    cam_pitch: float = -5.0
    cam_fov: float = 55.0

部屋の構成要素は以下の通りです。

  • 床 + 背面壁 + 天井 — 壁なし地下室スタイル(左右は開放)
  • PC デスク — テーブル板 + 4本脚
  • デスクトップモニター — ベゼル + スクリーン面 + スタンド
  • ノートPC — 底面 + 傾いた画面(laptop_tilt で角度指定)
  • 壁掛けテレビ — ベゼル + スクリーン面
  • 椅子 — 座面 + 背もたれ + 4本脚(chair_yaw で斜め配置)

ボックスメッシュの生成

各パーツは _box() ヘルパーで色付きボックスメッシュとして生成します。

def _box(width, height, depth, color=(0.5, 0.5, 0.5, 1.0)):
    """色付きボックスメッシュを生成。"""
    mesh = trimesh.creation.box(extents=[width, height, depth])
    mesh.visual.face_colors = np.tile(
        np.array(color) * 255, (len(mesh.faces), 1)
    ).astype(np.uint8)
    return mesh

配置は _translation()_rotation_x() / _rotation_y() で 4×4 変換行列を組み合わせて行います。ノート PC の画面は傾斜があるため、ヒンジ位置を原点にして _rotation_x(-cfg.laptop_tilt) で回転させてから平行移動します。

lid_pose = (
    _translation(laptop_x, hinge_y, hinge_z)
    @ _rotation_x(-cfg.laptop_tilt)
    @ _translation(0, bezel_h / 2, 0)
)

.glb エクスポートとスクリーン座標

構築したシーンは export_glb()room.glb(3D シーン)と screen_meta.json(スクリーン 4 隅座標 + カメラデフォルト値)に出力します。

screen_meta = {
    "camera_defaults": {
        "x": cfg.cam_x, "y": cfg.cam_y, "z": cfg.cam_z,
        "pitch": cfg.cam_pitch, "fov": cfg.cam_fov,
    },
    "screens": [
        {"name": name, "corners": corners.tolist()}
        for name, corners in screen_world_corners
    ],
}

スクリーンのワールド座標は、ジオメトリ構築時にベゼルの内側として計算しています。

Blender でデプスマップをレンダリング

render_depth.py は Blender のスクリプトとして動作し、.glb からデプスマップを出力します。2つの実行モードがあります。

ヘッドレスモード(カメラ位置は screen_meta.json のデフォルト値):

blender --background --python render_depth.py -- room.glb screen_meta.json output/

GUI モード(Blender 上でカメラを手動調整してからレンダリング):

blender scene.blend --python render_depth.py -- --from-blend screen_meta.json output/

GUI モードでは .glb をインポートしてカメラ位置を手動調整し、.blend として保存してからスクリプトを実行します。

座標系変換

room_scene.py (trimesh/glTF) は Y-up、Blender は Z-up です。screen_meta.json のワールド座標は Y-up のままなので、スクリプト内で明示変換します。

def yup_to_blender(xyz):
    """Y-up (glTF/OpenGL) → Blender Z-up 座標変換。"""
    return (xyz[0], -xyz[2], xyz[1])

Depth シェーダーの構成

デプスマップの描画にはコンポジターではなく マテリアルオーバーライド方式 を使っています。Blender 4.x / 5.x 両方で動作する方法です。ノード構成は以下の通りです。

Camera Data (View Z Depth)
    → Map Range (near=1.0/白, far=0.0/黒)
    → Emission (ライティングの影響を受けない)
    → Material Output

全メッシュのマテリアルをこのデプスマテリアルに一時的に差し替え、背景を黒にしてレンダリングします。Emission を使う理由は、ライティングの影響を受けず純粋な距離値だけを描画するためです。レンダリング後は元のマテリアルに自動復元します。

出力ファイル

レンダリングは depth.png(8bit グレースケール)、color.png(RGB プレビュー)、screen_regions.json(2D 射影座標)の 3 ファイルを出力します。screen_regions.json では world_to_camera_view() でワールド座標を 2D に変換しています。

パイプライン全体

ここまでの要素をつなげると、以下の 4 ステップのパイプラインになります。

Python (room_scene.py) → room.glb + screen_meta.json
        ↓
Blender (render_depth.py) → depth.png + screen_regions.json
        ↓
ComfyUI (Depth ControlNet) → 生成された壁紙画像
        ↓
screen_regions.json → 座標が事前に分かっている → 合成

このパイプラインの核心は最後の行です。スクリーンの座標は 3D モデルから計算済みなので、「検出」が不要です。前回までは生成画像に対して OpenCV でモニターを探す必要がありましたが、ここでは座標があらかじめ分かっています。

3Dでレンダリングした例。わかりやすく色をつけています

ComfyUI Depth ワークフロー設定

ComfyUI 側の設定は Canny の回と大きく変わりません。チェックポイントは同じ juggernautXL で、変更点は ControlNet モデルだけです。

設定項目
ControlNet モデルcontrol-lora-depth-rank256.safetensors
入力画像Blender 出力の depth.png
Strength0.7
Start %0
End %80

End % を 80% に設定しているのは、最後の 20% でモデルに自由度を与え、テクスチャや照明の自然さを確保するためです。

結果

Depth ControlNet + Blender の結果は、Canny と比べて明確に安定しました。

  • スクリーン位置の精度: 3D 射影で計算するため成功率はほぼ 100%。ControlNet の生成品質に依存しません
  • パースペクティブの一貫性: デプスマップが奥行きを指定するため、遠近感が毎回安定します
  • カメラアングルの調整: Blender GUI でカメラを動かすだけで自由に変えられます

Canny で悩まされた「モニターが歪む」「壁との距離感がおかしい」問題は、空間ごと制御することで根本的に解消されました。

元々の生成時のTVの位置をbboxで例示。当たり前ですが一致しています。

(参考)Blender との相性

Blender を使うワークフローは一見ハードルが高そうですが、実際にはかなり実用的です。ヘッドレスモードは CLI から一発で出力、GUI モードでは .glb をインポートしてカメラを手動調整してからスクリプトを実行します。color.png のプレビューができるので、AI 生成の前に構図を確認できる点も助かります。

また、今回は省略していますが、カメラの位置や構図などはBlenderで作業をした方が楽です。本記事では省略しますが、AI に相談しながら進めれば比較的簡単に進められます。

まとめ

  • Canny は 2D のエッジしか持たない ため、奥行き・パース・空間配置を制御できませんでした
  • Depth ControlNet はデプスマップで空間全体を指定でき、生成の安定性が段違いです
  • trimesh で 3D 部屋を構築 し、パラメータで家具やカメラ位置を調整できます
  • Blender でレンダリング するため、ヘッドレス自動化と GUI 手動調整の両方に対応します
  • スクリーン座標は 3D 射影で事前計算 されるため、検出処理が不要になりました

この組み合わせで、ようやく「安定してモニターのある部屋を生成し、スクリーン位置も正確に把握する」目標が達成できました。

次回は、このスクリーン領域に合成する 砂嵐ノイズコンテンツ の生成に進みます。レトロテレビらしい砂嵐をプログラムで作り、合成パイプラインに組み込む方法を解説します。お楽しみに。

タイトルとURLをコピーしました