3D-ассеты через агента Первая модель: модификаторы вместо ручных вершин
0%

Первая модель: модификаторы вместо ручных вершин

Первая модель: модификаторы вместо ручных вершин

Канонический первый объект — кружка. Она удачна тем, что содержит все базовые приёмы: тело вращения, полость, изогнутая деталь, фаски, сглаживание.

Весь код ниже уезжает в аргумент code инструмента execute_blender_code.

Чистим сцену — но осторожно

import bpy

def clear():
    # Не read_homefile(): перезагрузка файла посреди скрипта делает недействительным
    # контекст, в котором этот же скрипт исполняется, и все последующие
    # обращения к bpy.context падают.
    for o in list(bpy.data.objects):
        bpy.data.objects.remove(o, do_unlink=True)
    for coll in (bpy.data.meshes, bpy.data.curves, bpy.data.materials, bpy.data.lights):
        for d in list(coll):
            if d.users == 0:
                coll.remove(d)

Две ловушки сразу.

Первая — в комментарии. Очевидный способ очистить сцену, bpy.ops.wm.read_homefile(use_empty=True), для кода, который прямо сейчас исполняется, — самоубийство: перезагружается файл, а вместе с ним контекст.

Вторая — list(...). Удалять из коллекции, по которой идёт итерация, нельзя: итератор сдвигается, часть элементов пропускается. Копия списка снимает вопрос.

И запретное, из https://courses.digitable.life/post/3d-pipeline/02-setup/: bpy.ops.wm.read_factory_settings() вызывать нельзя никогда. Он сбрасывает настройки, а с ними отключает все аддоны — включая тот, который вас обслуживает. Связь умирает до перезапуска.

Тело: примитив плюс модификаторы

import math

R, H, WALL = 0.040, 0.095, 0.004   # радиус, высота, толщина стенки в метрах

bpy.ops.mesh.primitive_cylinder_add(vertices=96, radius=R, depth=H,
                                    location=(0, 0, H / 2))
body = bpy.context.active_object
body.name = "Mug"

location=(0, 0, H/2) ставит цилиндр на пол, а не по центру пола: примитив создаётся симметрично вокруг своего центра, и без сдвига половина кружки уйдёт под сцену.

Моделируйте в реальных единицах. Кружка радиусом 4 см — 0.040, а не 4.0. Масштаб всплывёт при экспорте (https://courses.digitable.life/post/3d-pipeline/06-export/), и разбираться потом дороже.

Вскрываем верх через bmesh

Кружка полая. Чтобы Solidify построил стенки, а не залил объём, сверху нужна дыра:

import bmesh

me = body.data
bm = bmesh.new()
bm.from_mesh(me)
bm.faces.ensure_lookup_table()
top = max(bm.faces, key=lambda f: f.calc_center_median().z)
bmesh.ops.delete(bm, geom=[top], context='FACES')
bm.to_mesh(me)
bm.free()

bmesh — низкоуровневое представление меша, где есть прямой доступ к вершинам, рёбрам и граням.

  • ensure_lookup_table() обязателен перед обращением по индексу. Без него — падение с невнятным сообщением.
  • Верхняя грань ищется по геометрии, а не по индексу: max по Z-координате центра. Индексы зависят от версии и параметров примитива, координаты — нет. Это устойчивый способ, и агенту стоит писать именно так.
  • bm.free() обязателен: bmesh не собирается сборщиком мусора.

Стенки, фаски, сглаживание

sol = body.modifiers.new("Solidify", 'SOLIDIFY')
sol.thickness = WALL
sol.offset = -1        # наращивать внутрь, сохраняя внешний силуэт

bev = body.modifiers.new("Bevel", 'BEVEL')
bev.width = 0.0012
bev.segments = 3
bev.limit_method = 'ANGLE'
bev.angle_limit = math.radians(35)

sol.offset = -1 — стенка растёт внутрь. При 0 она разошлась бы в обе стороны и внешний радиус перестал бы равняться R.

bev.limit_method = 'ANGLE' — важнее, чем кажется. По умолчанию фаска ложится на каждое ребро, включая те, что должны остаться острыми. Ограничение по углу оставляет фаску только там, где грани сходятся резче 35°.

Порядок модификаторов имеет значение: Solidify → Bevel → Subsurf. Фаска после подразделения тратит геометрию на рёбра, которые уже скруглены.

Ручка: кривая вместо меша

Ручку можно вылепить из цилиндров. Не надо — кривая Безье с толщиной даёт гладкую трубку одной настройкой:

cu = bpy.data.curves.new("HandleCurve", 'CURVE')
cu.dimensions = '3D'
cu.bevel_depth = 0.0055      # радиус трубки
cu.bevel_resolution = 6      # гладкость сечения
cu.resolution_u = 16         # гладкость вдоль пути
sp = cu.splines.new('BEZIER')
sp.bezier_points.add(3)      # add(n) добавляет n К уже существующей точке

bezier_points.add(3) даёт четыре точки: одна создаётся вместе со сплайном. Классическая ошибка на единицу.

Сглаживание

for p in body.data.polygons:
    p.use_smooth = True
bpy.ops.object.shade_auto_smooth(angle=math.radians(30))

Здесь версия имеет значение. Свойство mesh.use_auto_smooth, которое найдётся в любом туториале старше пары лет, в 4.1 удалено — вместо него оператор shade_auto_smooth. Проверено на 4.5.11: use_auto_smooth больше нет, shade_auto_smooth есть.

Это типичная беда генерации кода языковой моделью: в обучающих данных полно примеров под старый API. Признак — падение на строке, которая «всегда работала». Лечится проверкой у самого Blender:

print(hasattr(bpy.types.Mesh, "use_auto_smooth"))

Не спорьте с интерпретатором — спрашивайте его.

Почему у агента выходит именно такой стиль

Получившийся код — не имитация ручного моделирования, а его альтернатива, и она хорошо ложится на агента:

  • Параметры вместо вершин. «Стенку на миллиметр толще» — правка одной строки, а не перетаскивание сотни точек.
  • Модификаторы неразрушающие. Плохо легла фаска — меняется число, а не переделывается меш.
  • Код — это документация. R, H, WALL вверху файла объясняют модель лучше любого описания и позволяют собрать вариации.

Отсюда и граница применимости из https://courses.digitable.life/post/3d-pipeline/00-overview/: агент силён там, где форма описывается числами, и слаб там, где нужен глаз.

Дальше — https://courses.digitable.life/post/3d-pipeline/04-seeing/: как агент смотрит на сделанное и как незаметно слепнет.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов
Дальше