Community图像github.com

TraX22/blender-modeling

在用户运行的 Blender 中通过 BlenderLab 连接构建和编辑 3D 模型——通过逐步执行 bpy 代码、保持真实世界比例和整洁命名,并结合视口截图检查结果。适用于用户要求建模、构建、创建、修复或更改 Blender 中的对象或场景,或从描述或参考图像制作 3D 模型。

blender-modeling 是什么?

该技能使 AI 代理(如 Claude Code、Cursor 或 Codex)能够通过官方 BlenderLab 服务器直接与用户正在运行的 Blender 实例交互。它提供了一套工具,用于检查场景、执行 Python 代码、捕获视口截图、渲染和导出。核心工作流程遵循严格的“观察-规划-构建-验证-报告”循环,将模型分解为命名部件,以米为单位指定尺寸,逐步执行 30-90 行的 bpy 代码块,并通过视口截图或数字检查验证结果。该技能强调真实世界比例(1 单位 = 1 米)、使用 bpy.data 和 bmesh 而非上下文相关的运算符,以及可重复执行的脚本。它还可以处理参考图像,通过测量比例并与构建结果进行比较来确保保真度。最后一步 `final_check` 会分析模型的尺寸、亮度、饱和度和几何问题,确保质量。

兼容平台~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(按 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 打开且插件服务器已启动。不要将尚未完成的工作描述为已完成。

工作循环

每次都以以下顺序工作。跳过“先观察”和“后验证”步骤是导致模型出错而无人在意的原因。

  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。
  • 返回你需要的信息。 在调用结束时打印名称、尺寸或计数的简短摘要(在 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]                       # 第 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
    # 存储的名称以时间戳开头,容易复制错一位数字:
    # 回退到时间戳之后具有相同原始名称的最新上传文件。
    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.

相关技能