Community影像github.com

TraX22/blender-modeling

透過 BlenderLab 官方伺服器連接到使用者的 Blender,以逐步驗證的方式撰寫 bpy 程式碼來建立和編輯 3D 模型。支援真實世界比例、清晰命名、修改器,並透過視窗截圖檢查結果。當使用者要求在 Blender 中建模、建立、修改或修復物體或場景,或根據描述或參考圖像製作 3D 模型時使用。

blender-modeling 是什麼?

此 AI 技能名為「TraX22/blender-modeling」,旨在讓 AI 代理能夠直接在使用者正在執行的 Blender 實例中進行 3D 建模。它透過官方的 **BlenderLab** 伺服器連接運作,代理無法直接看到視窗,但可以透過一系列工具進行場景檢查、執行程式碼並擷取螢幕截圖。 這項技能的核心工作流程是「查看 → 規劃 → 分階段構建 → 驗證 → 報告」。代理必須先檢查場景原因,然後將模型分解為多個命名部件,並指定尺寸(以公尺為單位)和位置。接著,它會以小步驟(每個呼叫 30-90 行程式碼)進行構建,並在關鍵里程碑(例如主體結構、細節完成後)使用視窗截圖進行驗證,以確保比例和位置正確。這能有效防止常見的建模錯誤。 為了確保可靠性,這項技能設有嚴格的規則,例如:不得刪除或覆蓋未建立的物件、將所有工作放在獨立集合中、使用真實世界比例(1 單位 = 1 公尺)、以及優先使用 `bpy.data` 和 `bmesh` 等資料驅動的 API 而非依賴上下文的操作器。此外,它還提供一個「最終檢查」(`final_check`) 功能,能在最後一步自動渲染場景並分析主體比例、亮度、飽和度和顏色,若提供參考圖像還能進行比對。 此技能特別適合處理建築、家具、道具、機械零件等需要精確尺寸和結構的模型,也支援從參考圖像進行建模。它不支援內建的素材庫或 3D 生成器,因此需要代理自行建構模型或要求使用者提供要導入的檔案。此技能主要在 Claude Code 等支援 BlenderLab 連接的 AI 代理平台上運作。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/TraX22/HydraOps-Skills/tree/HEAD/blender-modeling

Installed? Explore more 影像 skills: steipete/songsee, affaan-m/frontend-design, mattpocock/improve-codebase-architecture · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

在 Blender 中建模

您透過 BlenderLab 連接 在使用者正在執行的 Blender 中工作。除非您要求截取畫面,否則您無法看到視埠,而且您所做的每個變更都是真實的:它會直接套用在使用者開啟的檔案中。

您的 Blender 連接

您透過 BlenderLab(官方 Blender Lab 伺服器)工作;其工具為 blenderlab_…:

  • 場景內容:get_objects_summary;單個物件的詳細資訊:get_object_detail_summary(按 名稱)。檔案(無論是否儲存)及其路徑: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 且開啟插件的伺服器。如果您無法完成工作,請勿將其描述為已完成。

工作循環

每次請按此順序進行。若跳過「先查看」和「後檢查」的步驟,模型最終可能會出錯而無人察覺。

  1. 查看。 使用場景工具了解現有內容(集合、物件、活動攝影機)。對於您將要觸及的物件,請使用物件詳細資訊工具。切勿假設場景為空。
  2. 規劃部件。 將模型分解為多個命名部件,並標明尺寸(以公尺為單位)和位置(例如「座椅 0.45 × 0.45 × 0.04,位於 z = 0.45」)。在開始建造任何大於幾個物件的內容之前,先以一個簡短列表陳述計畫。
  3. 分階段建造。 每次 execute_blender_code 呼叫處理兩到三個相關部件(例如牆壁、基座和屋頂;門和窗戶),約 30-90 行程式碼。當任務較複雜(如布林運算、曲面)或剛發生錯誤時,每次呼叫只處理一個部件:在一個 200 行的腳本中看到回溯追蹤,能提供的資訊很少,而且場景會被部分修改。
  4. 在里程碑處驗證。 在主體體積完成時、細節加入後以及最後,擷取 3D 視埠畫面,並實際與計畫進行比較:比例是否正確、部件是否在應在的位置對齊、沒有漂浮的部件、沒有部件嵌入其他部件內。在擷取之間,使用您的程式碼列印的數字進行檢查。在繼續前先修正問題。如果擷取結果顯示為您無法看到,表示您的模型無法看到影像:請改用數字進行驗證(尺寸、位置、計數),並說明您無法查看。
  5. 報告。 說明您建造了什麼、物件的名稱、尺寸,以及與要求不符的任何地方。

您的步驟預算

一項任務會給您有限次數的工具回合(預設 15 次,除非使用者提高),每次呼叫(開啟技能、場景摘要、建造呼叫、擷取)都會消耗一次。

  • 一次性開啟您需要的技能及其參考檔案,並且只在您會使用到它們時才開啟。不要在本次對話中重新開啟已開啟過的技能。
  • 在第一次建造呼叫之前,根據預算規劃階段:大約 1 回合用於查看,8-9 回合用於建造,3 次擷取,1 次備用於修正。優先處理最顯眼的部分。
  • 當擷取和摘要彼此不依賴時,可以在同一回合中同時要求兩者。
  • 如果請求無法完全滿足,請在計畫中說明,先完成其中一個連貫的部分,並在最後列出尚未完成的確切清單,以便使用者只需說「繼續」。切勿在部件完成一半時停止。

預防大部分失敗的規則

  • 除非使用者要求,否則不要刪除或覆蓋您未建立的內容。 不得使用「全選,刪除」來清空場景;不得使用 read_factory_settings。如果預設的立方體造成阻礙,請詢問使用者或在旁邊工作。
  • 將您的工作放在自己的集合中,以模型命名,並為每個物件和網格賦予真實名稱(例如 Chair_Leg_FL,而非 Cube.004)。後續步驟會按名稱尋找物件。
  • 真實比例。 1 單位 = 1 公尺。門高 2.0 公尺,馬克杯高 0.10 公尺。建造後請檢查 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。在編寫任務的第一個腳本前請先開啟它。

選擇如何建構形狀

形狀方法
方塊、圓柱體、家具、建築、道具基本體,縮放並定位;使用「倒角修改器」處理邊緣
重複的項目(柵欄、樓梯、齒輪的齒)一個零件 + 陣列修改器,或一個連接相同網格的迴圈
圓形或車削物體(花瓶、玻璃杯、瓶子、輪子)一個輪廓 + 螺絲修改器,或一條帶有倒角的曲線
管道、電纜、把手、軌道一條具有 bevel_depth 的曲線
對稱物體(車輛、角色粗模)半個模型 + 鏡像修改器
文字、標誌、硬幣、牌匾文字或曲線物件,擠出後轉換為網格
柔軟、有機的形狀從基本體進行粗模 + 細分曲面修改器;誠實說明以這種方式處理雕刻細節有限制

迭代時請保持修改器不套用(保持可編輯狀態),並在報告中說明;僅在需要匯出或後續步驟需要真實幾何體時才套用它們。

從參考圖像工作

  • 首先描述您看到的內容:部件、它們之間的相對比例,以及任何重複項目的數量。根據這個列表進行建構。
  • 由影像決定,而非為其編造的數字。 解讀比例作為比值(寬度對高度對深度;屋頂佔總高度的比例;門相對於其牆壁的寬度),選擇一個測量值作為縮放基準(使用者的,或一個合理的真實世界尺寸),並根據比值推導出所有其他尺寸。如果請求的文字給出的尺寸與影像衝突,請遵循影像並說明。
  • 當您被要求根據參考資料撰寫提示或簡報時,請描述形狀、比例、顏色和數量。不要編造以公尺為單位的尺寸:根據該簡報建構的人會遵循這些尺寸而不是圖片。
  • 建構參考資料顯示的內容。除非使用者要求,否則不要添加參考資料沒有的底座、道具或裝飾。
  • 在粗模完成後,以及在報告之前,將視圖置於與參考資料相同的角度,擷取它並逐項比較:輪廓、比例、每個部件的位置、顏色。修正差異;無法修正的內容,請在報告中列出。預計需要兩到三次修正回合;這是正常的,並非失敗。
  • 取景、光線和顏色是透過測量而非目視判斷:請參閱下方的「最終檢查」。當您提供參考檔案的名稱時,它會將您的渲染圖與參考檔案進行比較。
  • 如果參考資料附加在較早的訊息中,您可能無法再看到它。如果您看不到,請說明並要求再次提供,而不是憑記憶工作。

最終檢查(始終作為您的最後一次呼叫)

您看不到您的渲染結果:渲染工具會寫入一個檔案並給您其路徑,而視埠截圖不會顯示場景的光線。一個根據其意圖而非實際輸出進行報告的模型,會輸出白色或黑色影像而不自知。因此,每個會建構或修改模型的任務的最後一個 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]                       # row 0 is the bottom of the image
    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)])
    # Background = close to one of the corner colors (a gradient or a soft shadow still counts).
    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

# Where a HydraOps installation keeps its data (storage/uploads hangs from it): the data
# folder it was told (HYDRA_DATA_DIR), the installer's folder per system, and a clone of
# the repository, which keeps its data inside itself when run from source.
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):
    """The path of an image attached in THIS conversation, named as the conversation shows it:
    "storage/uploads/1791118524833-house.png", the bare "1791118524833-house.png", or a full path.
    None when it is not there."""
    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
    # The stored name starts with a timestamp that is easy to copy with one digit wrong:
    # fall back to the newest upload with the same original name after the timestamp.
    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:

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

相關技能