Community이미지github.com

TraX22/blender-modeling

BlenderLab 연결을 통해 사용자의 Blender에서 3D 모델을 생성하고 편집합니다. 씬을 검사하고, 작은 단위로 검증된 bpy 코드를 작성하며, 실제 세계 스케일과 깔끔한 명명, 모디파이어를 사용하고 뷰포트 스크린샷으로 결과를 확인합니다. 사용자가 Blender에서 객체나 씬을 모델링, 생성, 수정하거나, 설명 또는 참조 이미지로 3D 모델을 만들라고 요청할 때 사용하세요.

blender-modeling란 무엇인가요?

이 스킬은 BlenderLab 서버 연결을 통해 사용자의 Blender와 직접 상호작용하여 3D 모델링 작업을 수행합니다. 씬 요약, 객체 세부 정보 조회, bpy 코드 실행, 뷰포트 스크린샷 캡처 등 다양한 도구를 제공하며, 실제 세계 스케일(1 유닛 = 1 미터)과 체계적인 명명 규칙을 따릅니다. 스킬은 먼저 씬을 검사하고, 모델을 부품별로 계획한 후, 작은 검증된 단계로 빌드하고, 중간 중간 뷰포트 캡처로 결과를 확인하는 반복적인 워크플로우를 강조합니다. 실패를 방지하기 위해 기존 객체를 덮어쓰지 말고, 자신만의 컬렉션에 작업하며, 가능한 경우 bpy.ops 대신 bpy.data와 bmesh를 사용하는 규칙을 포함합니다. 각 스킬 호출은 제한된 횟수(기본 15회)를 사용하므로, 계획을 신중하게 세워야 합니다. 최종적으로 자동으로 렌더링하여 프레임, 밝기, 채도, 메인 컬러, 깨진 지오메트리 등을 확인하고, 참조 이미지가 있을 경우 비교 분석합니다. 참조 이미지로 작업할 때는 비율을 측정하고 눈대중이 아닌 숫자로 검증해야 하며, 모델이 이미지를 '볼 수 없는' 경우(가시성 문제)에도 숫자를 통해 검증할 수 있습니다.

지원 대상~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에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

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에 할당된 dict가 데이터로 반환됩니다. 가드는 몇 가지 연산자(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.0m, 머그컵은 0.10m입니다. 입력한 숫자를 믿지 말고 구축 후 obj.dimensions를 확인하세요.
  • 변환: location, rotation_euler (라디안: math.radians 사용) 및 scale을 직접 설정하세요. 베벨이나 내보내기 전에 스케일을 적용하세요(참조 자료 참조).
  • 연산자보다 데이터를 선호하세요. bpy.data와 bmesh는 선택 상태에 관계없이 작동합니다. bpy.ops는 컨텍스트에 의존하며 읽기 어려운 방식으로 실패합니다. 연산자를 사용해야 하는 경우 먼저 객체를 명시적으로 선택하고 활성화하세요.
  • 스크립트를 재실행 가능하게 만드세요. 매번 Leg.001, Leg.002를 생성하는 대신 이름으로 객체를 찾아 재사용하거나 교체하세요.
  • 필요한 것을 다시 보내세요. 호출을 이름, 크기 또는 개수에 대한 짧은 print(BlenderLab에서, 또는 result에 할당된 dict)로 끝내서 다음 단계가 사실에서 시작하도록 하세요.
  • 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):
    """An image split into background (the colors of its four corners) and subject (the rest):
    how much of the image the subject takes, its box, its brightness, saturation and colors."""
    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:
                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

Individual skills in this repo

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

관련 스킬