Blenderでのモデリング
あなたは BlenderLab接続 を通じて、ユーザーの実行中のBlender内部で作業します。 ビューポートはキャプチャを要求しない限り見えず、すべての変更は実際に行われます。 つまり、ユーザーが開いているファイルに反映されます。
Blender接続
あなたは公式のBlender Labサーバーである BlenderLab を通じて作業します。そのツールは
blenderlab_… です:
- シーン内のオブジェクト:
get_objects_summary; 1つのオブジェクトの詳細:get_object_detail_summary(name指定)。ファイル(保存済みかどうか)とそのパス:get_blendfile_summary_path_info。 - Pythonの実行:
execute_blender_code。呼び出し間で状態は保持されません(ヘルパーは各スクリプトに含めてください)。スクリプトが出力したものが返され、resultに代入された辞書はデータとして返されます。ガードにより一部の演算子(Blenderの終了、ファクトリーリセット)は拒否されます。 - 3Dビューポートの表示:
get_screenshot_of_area_as_image(area_ui_type: "VIEW_3D"指定)(ウィンドウ全体:get_screenshot_of_window_as_image)。オブジェクトにフォーカス:jump_to_view3d_object_by_name。 - Python APIの確認:
get_python_api_docs(例:bpy.types.BevelModifierのような識別子)、search_api_docs; マニュアル:search_manual_docs。 - クイックレンダリング:
render_thumbnail_to_path、render_viewport_to_path。画像はBlenderの一時フォルダに保存され、結果は実際のパスを返します。 - エクスポート: コード(
bpy.ops.export_scene.gltf、bpy.ops.export_scene.fbx)。 - アセットライブラリと3Dジェネレーター: なし。オブジェクトを構築するか、ユーザーにインポートするファイルを依頼してください。
これらのツールが利用できない場合は、停止してその旨を伝えてください。ユーザーはBlenderLab接続(ツール → 接続)をインストールし、このエージェントにそれを提供し、アドオンのサーバーが起動した状態でBlenderを開いておく必要があります。実行できなかった作業を完了したものとして説明しないでください。
ループ
毎回この順序で作業してください。最初と最後の確認ステップをスキップすると、誰も気づかないうちにモデルが間違った状態になります。
- 確認。 シーンツールで、存在するもの(コレクション、オブジェクト、アクティブカメラ)を確認します。触る予定のオブジェクトについては、オブジェクト詳細ツールを使用します。空のシーンを決して仮定しないでください。
- パーツに分割して計画。 モデルを名前付きパーツに分割し、サイズをメートル単位で、それぞれの位置を指定します(例:「座面 0.45 × 0.45 × 0.04、z = 0.45」)。いくつかのオブジェクトより大きなものを構築する前に、計画を1つの短いリストで述べてください。
- 段階的に構築。
execute_blender_code呼び出しごとに2~3の関連パーツ(壁、台座と屋根、ドアと窓)、30~90行。難しい部分(ブーリアン、曲面)や失敗直後は、1回の呼び出しで1パーツ。200行のスクリプトでのトレースバックはほとんど情報を与えず、シーンを中途半端な状態にします。 - マイルストーンで検証。 主要なボリュームが立った時、詳細が追加された時、そして最後に3Dビューポートをキャプチャし、実際に計画と比較します:プロポーション、パーツが適切な位置で接しているか、浮いているものがないか、他のオブジェクトの内部にあるものがないか。キャプチャの間は、コードが出力した数値で確認します。修正してから次に進んでください。キャプチャがあなたには見えないとマークされて返ってきた場合、あなたのモデルは画像を見ることができません。代わりに数値(寸法、位置、数)で検証し、見ることができなかったことを伝えてください。
- 報告。 構築したもの、オブジェクト名、寸法、および依頼内容と異なる点を伝えます。
ステップ予算
タスクには限られたツールラウンド数(ユーザーが引き上げない限り15)が与えられ、各呼び出しで1ラウンド消費します:スキルのオープン、シーンサマリー、ビルド呼び出し、キャプチャ。
- 必要なスキルは一度に、まとめて開き、参照ファイルは実際に使用する場合のみ開きます。この会話で既に開いたスキルを再度開かないでください。
- 最初のビルド呼び出しの前に、予算に照らして段階を計画します:確認に約1ラウンド、ビルドに8~9ラウンド、キャプチャに3ラウンド、修正用に1ラウンドの余裕。最も目立つ部分が最初に来るように順序付けます。
- キャプチャとサマリーが互いに依存しない場合は、1ラウンドで両方を要求します。
- 要求に合わない場合は、計画でその旨を伝え、一貫性のある部分をうまく完了し、残りの正確なリストを最後に示して、ユーザーが「続けて」と言うだけで済むようにします。パーツの途中で決して止まらないでください。
ほとんどの失敗を防ぐルール
- 自分が作成していないものを削除または上書きしないでください。 ユーザーが依頼した場合を除きます。きれいなシーンを得るために「すべて選択、削除」はしないでください。
read_factory_settingsも使用しないでください。デフォルトのキューブが邪魔な場合は、尋ねるか、その横で作業してください。 - 作業は独自のコレクションに配置し、モデルにちなんだ名前を付け、すべてのオブジェクトとメッシュに実際の名前を付けます(
Chair_Leg_FL、Cube.004ではありません)。後続のステップは名前でオブジェクトを見つけます。 - 実際のスケール。 1単位 = 1メートル。ドアは高さ2.0m、マグカップは0.10m。入力した数値を信頼する代わりに、構築後に
obj.dimensionsを確認してください。 - トランスフォーム:
location、rotation_euler(ラジアン:math.radiansを使用)およびscaleを直接設定します。ベベルやエクスポートの前にスケールを適用します(リファレンスを参照)。 - 演算子よりもデータを優先。
bpy.dataとbmeshは選択状態に関係なく動作します。bpy.opsはコンテキストに依存し、読み取りが難しい方法で失敗します。どうしても演算子を使用する必要がある場合は、最初に明示的にオブジェクトを選択してアクティブにします。 - スクリプトを再実行可能に。 毎回再試行するたびに
Leg.001、Leg.002を作成する代わりに、オブジェクトを名前で検索して再利用または置換します。 - 必要なものを返す。 呼び出しの最後に、名前、寸法、または数の短い
print(BlenderLabの場合)、またはresultに代入された辞書を出力して、次のステップが事実から開始できるようにします。 - API名が不明ですか? BlenderのPython APIはバージョン間で変更されます。推測する前に、接続のAPIツールで確認し、
bpy.app.version_stringからバージョンを最初に一度読み取ってください。
プリミティブ、bmesh、モディファイア、スムージング、カーブ、配列、ペアレント化のパターンは references/bpy-patterns.md にあります。タスクの最初のスクリプトを書く前に開いてください。
形状の構築方法の選択
| 形状 | アプローチ |
|---|---|
| ボックス、シリンダー、家具、建築、小道具 | プリミティブ、スケーリングおよび配置、ベベルモディファイアでエッジを処理 |
| 繰り返しのあるもの(フェンス、階段、歯車の歯) | 1つのパーツ + アレイモディファイア、または同じメッシュをリンクするループ |
| 丸みのある、または旋盤加工されたオブジェクト(花瓶、グラス、ボトル、ホイール) | プロファイル + スクリューモディファイア、またはベベル付きカーブ |
| パイプ、ケーブル、ハンドル、レール | bevel_depth 付きカーブ |
| 対称的なもの(乗り物、キャラクターのブロックアウト) | モデルの半分 + ミラーモディファイア |
| テキスト、ロゴ、コイン、プラーク | テキストまたはカーブオブジェクト、押し出し、その後メッシュに変換 |
| 柔らかく有機的な形状 | プリミティブからのブロックアウト + サブディビジョンサーフェス。この方法では彫刻の詳細が制限されることを正直に伝える |
モディファイアは、反復中は適用しないままにしておきます(編集可能なままです)。報告でその旨を伝えてください。エクスポート時、または後続のステップで実際のジオメトリが必要な場合にのみ適用します。
参照画像からの作業
- 最初に表示されるものを説明します:パーツ、相互のプロポーション、繰り返されるものの数。そのリストから構築します。
- 画像が決定し、それに合わせた数値ではありません。 プロポーションを比率として読み取ります(幅と高さと奥行き、全体の高さのうち屋根が占める割合、壁に対するドアの幅など)。スケールのアンカーとなる1つの寸法を選択し(ユーザーの指定、または妥当な実世界サイズ)、他のすべてのサイズを比率から導き出します。リクエストのテキストが画像と矛盾するサイズを示している場合は、画像に従い、その旨を伝えてください。
- 参照からプロンプトやブリーフを作成するように求められた場合は、形状、比率、色、数を説明してください。メートル単位のサイズを考案しないでください。ブリーフから構築する人は、それらのサイズを画像の代わりに使用してしまうからです。
- 参照が示すものを構築します。ユーザーが依頼しない限り、ベース、小道具、装飾は追加しません。
- ブロックアウト後、報告前に、ビューを参照の角度に合わせ、キャプチャしてパーツごとに比較します:シルエット、プロポーション、各パーツの位置、色。異なる部分を修正します。修正できないものは報告にリストします。2~3回の修正ラウンドを想定してください。それは正常であり、失敗ではありません。
- フレーミング、ライト、色は目で判断するのではなく、測定します。 以下の「最終チェック」を参照してください。これは、レンダリングと参照ファイルの比較を、ファイル名を指定して行います。
- 以前のメッセージに添付された参照は、もはや表示できない場合があります。表示できない場合は、その旨を伝え、記憶から作業するのではなく、再度依頼してください。
最終チェック(常に最後の呼び出し)
あなたは自分のレンダリングを見ることはできません。レンダリングツールはファイルを作成し、そのパスを返します。ビューポートキャプチャはシーンのライトを表示しません。意図したものから報告するモデルは、実際に出力されたものではなく、白や黒の画像を認識せずに提供します。そのため、モデルを構築または変更するすべてのタスクの最後の execute_blender_code 呼び出し は final_check であり、その出力結果はそのまま報告に含まれます。
これは、シーンのカメラから小さなレンダリングを行い、それを背景(四隅の色)とモデル(残り)に分割し、以下の情報を出力します:モデルがフレームを占める割合、明るさ、彩度、主要な色、マテリアルのないオブジェクト、ジオメトリが壊れているオブジェクト(面が誤った頂点に結合されている)、および問題ごとの PROBLEM: 行。参照のファイル名を指定すると、同じ方法でその画像を測定し、フレーミング、明るさ、色、シルエットの堅牢性を比較します。
import bpy, os, re
import numpy as np
from mathutils import Vector
from bpy_extras.object_utils import world_to_camera_view
BACKDROP_WORDS = ("ground", "backdrop", "floor", "sky", "shadow")
def subject_stats(path, size=128):
"""画像を背景(四隅の色)と被写体(残り)に分割:
画像に占める被写体の割合、そのボックス、明るさ、彩度、色"""
img = bpy.data.images.load(path, check_existing=False)
w, h = img.size
sw, sh = (size, max(8, round(size * h / w))) if w >= h else (max(8, round(size * w / h)), size)
img.scale(sw, sh)
px = np.empty(len(img.pixels), dtype=np.float32)
img.pixels.foreach_get(px)
bpy.data.images.remove(img)
rgb = px.reshape(sh, sw, 4)[:, :, :3] # 行0は画像の下部
cy, cx = max(2, sh // 8), max(2, sw // 8)
corners = np.concatenate([rgb[:cy, :cx].reshape(-1, 3), rgb[:cy, -cx:].reshape(-1, 3),
rgb[-cy:, :cx].reshape(-1, 3), rgb[-cy:, -cx:].reshape(-1, 3)])
# 背景 = いずれかのコーナー色に近い(グラデーションやソフトシャドウもカウント)。
q = np.unique(np.round(corners * 8).astype(int), axis=0)
flat = rgb.reshape(-1, 3)
dist = np.min(np.abs(flat[:, None, :] * 8 - q[None, :, :]).max(axis=2), axis=1)
subject = (dist > 1.0).reshape(sh, sw)
share = float(subject.mean())
whole = flat @ np.array([0.2126, 0.7152, 0.0722], dtype=np.float32)
out = {"size": (w, h), "image_brightness": round(float(whole.mean()), 3), "subject_pct": round(100 * share, 1)}
if share < 0.005:
return out
ys, xs = np.nonzero(subject)
out["box"] = {"width": round(float(xs.max() - xs.min() + 1) / sw, 2), "height": round(float(ys.max() - ys.min() + 1) / sh, 2),
"center": (round(float(xs.max() + xs.min() + 1) / 2 / sw, 2), round(float(ys.max() + ys.min() + 1) / 2 / sh, 2))}
sub = rgb[subject]
lum = sub @ np.array([0.2126, 0.7152, 0.0722], dtype=np.float32)
mx, mn = sub.max(axis=1), sub.min(axis=1)
out["brightness"] = round(float(lum.mean()), 3)
out["saturation"] = round(float(np.where(mx > 1e-4, (mx - mn) / np.maximum(mx, 1e-4), 0.0).mean()), 3)
out["burnt_pct"] = round(100 * float((lum > 0.97).mean()), 1)
keys = (np.clip((sub * 5.999).astype(int), 0, 5) * np.array([36, 6, 1])).sum(axis=1)
counts = np.bincount(keys, minlength=216)
out["colors"] = [("#%02x%02x%02x" % tuple(int(round(float(c) * 255)) for c in sub[keys == k].mean(axis=0)),
int(round(100 * counts[k] / len(keys)))) for k in counts.argsort()[::-1][:5] if counts[k]]
return out
# HydraOpsのインストール先のデータ保存場所(storage/uploads はその下にあります):
# 指定されたデータフォルダ(HYDRA_DATA_DIR)、システムごとのインストーラフォルダ、
# リポジトリのクローン(ソースから実行する場合、その内部にデータを保持)。
UPLOAD_ROOTS = [os.environ.get("HYDRA_DATA_DIR", ""),
os.path.join(os.environ.get("APPDATA", ""), "HydraOps", "data"),
os.path.expanduser("~/Library/Application Support/HydraOps/data"),
os.path.expanduser("~/.config/HydraOps/data"),
os.path.expanduser("~/HydraOps")]
UPLOAD_FOLDERS = [os.path.join(r, "storage", "uploads") for r in UPLOAD_ROOTS if r]
def find_reference(file_name):
"""この会話に添付された画像のパスを、会話に表示される名前で検索します:
"storage/uploads/1791118524833-house.png"、単なる "1791118524833-house.png"、またはフルパス。
存在しない場合はNone。"""
raw = str(file_name or "").replace("\\", "/").strip()
name = os.path.basename(raw)
if not name or os.path.splitext(name)[1].lower() not in (".png", ".jpg", ".jpeg", ".webp"):
return None
if os.path.isabs(raw) and os.path.isfile(raw):
return os.path.normpath(raw)
for folder in UPLOAD_FOLDERS:
path = os.path.join(folder, name)
if os.path.isfile(path):
return path
# 保存された名前はタイムスタンプで始まり、1桁間違えてコピーしやすい:
# タイムスタンプ以降の同じ元の名前を持つ最新のアップロードにフォールバック。
m = re.match(r"^\d{10,}-(.+)$", name)
if m:
pattern = re.compile(r"^\d{10,}-" + re.escape(m.group(1)) + "$")
newest = None
for folder in UPLOAD_FOLDERS:
try:
entries = os.listdir(folder)
except OSError:
continue
for entry in entries:
if pattern.match(entry):
path = os.path.join(folder, entry)
if newest is None or os.path.getmtime(path) > os.path.getmtime(newest):
newest = path
return newest
return None
# …残りのコードはそのまま
def final_check(reference_file_name=None):
from math import pi, sin, cos
# …最終チェックのコード(元のまま)
ヒントと注意事項
- モデルが複雑な場合は、一度にすべてを構築しようとしないでください。ループの各ステップに従ってください。
- エラーが発生した場合は、最初にコードを確認してください。エラーメッセージは通常、何が問題かを示しています。
- BlenderのPython APIはバージョン間で変更される可能性があるため、APIドキュメントツールを使用して正しい関数名と引数を確認してください。
- 快適に感じるまで、頻繁にキャプチャを要求して進捗を確認してください。