Система скриптового 3д моделирования ZenCad
Та работа, которую я хочу представить вашему вниманию, есть попытка еще раз написать систему скриптового 3д моделирования. Написать так, как я её хотел бы видеть.
Рецепт системы ZenCad довольно прост. Концепт скриптового 3д моделирования OpenScad, геометрическое ядро OpenCascade, python3 в качестве клея, библиотека ленивых вычислений evalcache для агресивного кеширования вычислений. Добавить специй еще из пары-тройки инструментов, приправить gui под соусом PyQt, и подать к столу перемешав, но не взбалтывая.
Скриптовые CAD системы
CAD, или по нашему САПР, есть система автоматизированного проектирования. В отличии от интерактивных 3д редакторов скриптовые CAD системы трактуют слово «автоматизированое» в том значении, в котором обычно понимают его создатели програмного обеспечения. Тоесть не как набор вычислительных инструментов, но как кристально чистый алгоритм, требующий вмешательства человека лишь в момент его создания.
Работая в скриптовом CAD мы не рисуем модель непосредственно на экране, но выстраиваем программу, согласно которой генерируется модель. Для людей, знакомых только с интерактивными CAD, этот подход можно описать как возведенное в абсолют параметрическое моделирование. Строго говоря, скрипты часто применяются для дополнения интерактивной среды, для написания дополнительных инструментов, но работа в парадигмально чистом скриптовом CAD требует другой организации рабочего процесса, образа мысли и расчитана на несколько отличный круг задач.
Самым известным и чистым скриптовым CAD на текущий момент является OpenScad.
В начале был OpenScad
Есть определенный круг людей, кто предпочитает удобному Компасу, SolidWorks, FreeCad приземлённый и неприхотливый OpenScad. Довольно непросто ответить на вопрос в чем секрет его успеха, но точно можно сказать, что он лёгок, достаточно гибок в использовании, имеет минимум настроек. Части моделей, написанных на нем легко переиспользовать.
Однако, у openscad есть несколько обидных недостатков:
- openscad работает только с mesh сетью.
- openscad имеет довольно низкий предел масштабируемости, начинает сильно лагать на больших моделях.
- openscad довольно сложно интегрировать с другими системами, причиной чему использование собственного языка.
К сожалению, при всем могуществе скриптового подхода, выйти за рамки утилитарного наколечного написания простых моделей с OpenScad довольно проблематично.
Язык и все-все-все
Первое, что здесь хочется поправить — взять в качестве боевого инструмента язык общего назначения. Использование языка общего назначения позволяет использовать полноту его синтаксических возможностей и совокупность написанных ранее библиотек для решения задач 3д моделирования.
Интерфейс ZenCad:
Интерфейс OpenScad:
Применение python позволяет упростить код openscad, сделав код модели прозрачнее в сравнении с OpenScad.
#!/usr/bin/env python
#coding: utf-8
from zencad import *
lazy.diag = True
c1 = 100
c2 = 130
c3 = c2/2 + 20
base = box(c1,c1,c1,center=True)
f1 = ngon(r = 35, n = 3)
f2 = ngon(r = 35, n = 5)
f3 = circle(35)
s1 = linear_extrude(f1, c2, center=True)
s2 = linear_extrude(f2, c2, center=True).rotateY(deg(90))
s3 = linear_extrude(f3, c2, center=True).rotateX(deg(90))
# Обратите внимание на применение операторов к булевым операциям над 3д телами.
m1 = base - s1 - s2 - s3
m2 = base ^ s1 ^ s2 ^ s3
m3 = s1 + s2 + s3
ystep = 240
xstep = 240
fontpath = os.path.join(zencad.moduledir, "examples/fonts/testfont.ttf")
# Надписи являются обычными объектами. К ним применимы все стандартные операции.
t1 = textshape("difference", fontpath, 40)
t1c = t1.center()
t1=t1.translate(-t1c.x, -t1c.y, 0).rotateZ(deg(45))
t2 = textshape("intersect", fontpath, 40)
t2c = t2.center()
t2=t2.translate(-t2c.x, -t2c.y, 0).rotateZ(deg(45))
t3 = textshape("union", fontpath, 40)
t3c = t3.center()
t3=t3.translate(-t3c.x, -t3c.y, 0).rotateZ(deg(45))
# И наконец, размечаем сцену.
disp(base.forw(ystep))
disp(s1)
disp(s2.left(xstep))
disp(s3.right(xstep))
disp(m1.back(ystep))
disp(m2.left(xstep).back(ystep))
disp(m3.right(xstep).back(ystep))
disp(t1.back(ystep).up(c3), Color(1,1,0))
disp(t2.left(xstep).back(ystep).up(c3), Color(1,1,0))
disp(t3.right(xstep).back(ystep).up(c3), Color(1,1,0))
disp(s1.left(xstep).back(ystep), Color(0.5,0,0,0.95))
disp(s2.left(xstep).back(ystep), Color(0.5,0,0,0.95))
disp(s3.left(xstep).back(ystep), Color(0.5,0,0,0.95))
disp(s1.back(ystep), Color(0.5,0,0,0.95))
disp(s2.back(ystep), Color(0.5,0,0,0.95))
disp(s3.back(ystep), Color(0.5,0,0,0.95))
show()
Очень удобно, например, фильтровать облако точек с применением синтаксиса генераторов.
#!/usr/bin/env python3
from zencad import *
# Строим шестиугольник.
ng = ngon(r = 10, n = 6)
# Получаем его вершины и фильтруем нужные.
vertices = ng.vertices()
filtered_vertices = [v for v in vertices if v.x < 0]
# Применяем операцию скругления в полученных вершинах.
m = ng.fillet(4, filtered_vertices)
disp(m)
show()
Благодаря python, неофициально занимающему в современной програмной экосистеме титул короля клея, zencad легко интегрируется с другими библиотеками и програмными комплексами. Мы можем в одном скрипте использовать sympy для генерации аналитической поверхности, numpy для обработки сгенерированного по этой поверхности облака точек и, конечно же, zencad для построения, визуализации и постобработки.
from zencad import *
import numpy
xcoords = numpy.linspace(-10,10,50)
ycoords = numpy.linspace(-10,15,50)
lines = [ interpolate([point(x, y, 0.01*(x**2 + y**3)) for x in xcoords]) for y in ycoords ]
wires = []
for l in lines:
trans = translate(0,0,-30)
sf = l.endpoints()
w=sew([l, segment(sf[0], trans(sf[0])), trans(l), segment(sf[1], trans(sf[1]))])
wires.append(w)
for l in lines:
disp(l.left(30))
disp(loft(wires) - halfspace().down(10))
show()
Прочный фундамент OpenCascade
Математика полигональных сеток значительно проще математики граничного представления, но граничное представление гораздо практичнее. В частности, у полигональных сеток есть проблема комбинаторного взрыва, которая проявляется особенно, когда наступает время рендерить модель. В OpenScad зачастую приходится разрабатывать изделие с разрешением значительно более маленьким, нежели разрешение реальной модели, что нарушает чистоту парадигмы.
Таким образом, второй точкой внедрения является использование полноценного геометрического ядра, использующего граничное представление вместо мешсетевой модели. Построенный вокруг хакерского геометрического ядра OpenCascade, ZenCad, конечно же, не ставит целью предоставить полноту его возможностей в среде python. Попытка в полной мере передать OpenCascade, привела бы к написанию второго pythonOCC. ZenCad берет по верхам, стараясь сохранить баланс между функциональностью и эргономичностью.
#!/usr/bin/env python3
#coding: utf-8
from zencad import *
import zencad.surface as surface
import zencad.curve2 as curve2
lazy.diag=True
height = 70
width = 50
thickness = 30
# BASE
pnt1 = point(-width/2,0,0);
pnt2 = point(-width/2,-thickness/4,0);
pnt3 = point(0,-thickness/2,0);
pnt4 = point(width/2,-thickness/4,0);
pnt5 = point(width/2,0,0);
edge1 = segment(pnt1, pnt2)
edge2 = circle_arc(pnt2, pnt3, pnt4)
edge3 = segment(pnt4, pnt5)
wire = sew([edge1, edge2, edge3])
profile = sew([wire, wire.mirrorX()])
body = profile.fill().extrude(height)
body = fillet(body, thickness/12)
hl(body.forw(140))
# NECK
neck_radius = thickness/4.;
neck_height = height/10;
neck = cylinder(r=neck_radius, h=neck_height).up(height)
body = body + neck
hl(body.forw(100))
# THICK
body = thicksolid(body, -thickness / 50, [point(0,0,height+height/10)])
hl(body.forw(60))
# THREAD (Поддержка 2д кривых на поверхностях сейчас в экспериментальном режиме.)
cylsurf1 = surface.cylinder(neck_radius * 0.99)
cylsurf2 = surface.cylinder(neck_radius * 1.05)
major = 2 * math.pi;
minor = neck_height / 10;
angle = math.atan2(neck_height / 4, 2 * math.pi)
ellipse1 = curve2.ellipse(major, minor).rotate(angle)
arc1 = cylsurf1.map(curve2.trimmed_curve2(ellipse1, 0, math.pi))
segment1 = cylsurf1.map(curve2.segment(ellipse1.value(0), ellipse1.value(math.pi)))
ellipse2 = curve2.ellipse(major, minor/4).rotate(angle)
arc2 = cylsurf2.map(curve2.trimmed_curve2(ellipse2, 0, math.pi))
segment2 = cylsurf2.map(curve2.segment(ellipse2.value(0), ellipse2.value(math.pi)))
m1 = sew([arc1, segment1])
m2 = sew([arc2, segment2])
thread = loft([m1, m2]).up(height + neck_height / 2)
hl(m1.up(height + neck_height / 2).right(80))
hl(m2.up(height + neck_height / 2).right(60))
hl(thread.right(40))
# FINAL
m = thread + body
display(m)
show()
Преемственность традиции. Начало координат как исток всего
Синтаксические решения zencad, по примеру его старшего брата и учителя OpenScad, минимизируют количество сущностей в библиотеке. Как и OpenScad, ZenCad принципиально не умеет создавать примитив в точке (x, y, z), несмотря на то, что OpenCascade это позволяет. ZenCad сначала создает примитив в начале координат, после чего задает необходимое ему положение, применяя преобразования. Преобразования в ZenCad существуют и как отдельные объекты и как методы тел.
# Синтаксис методов.
cube(40, center=True).rotateX(deg(45)).rotateZ(deg(45)).right(20)
# Синтаксис объектов.
(right(20) * rotateZ(deg(45)) * rotateX(deg(45)))(cube(40, center=True))
# Или так.
trans = right(20) * rotateZ(deg(45)) * rotateX(deg(45))
cube(40, center=True).transform(trans)
Набор преобразований стандартен и включает трансляцию, вращения, отражения и изменения масштаба.
Лень
С целью минимизации времени вычислений, математика в ZenCad ленифицирована, а все вычисления агрессивно закешированы. Управление алгоритмами ленификации берет на себя [del]блокчейн[/del] библиотека evalcache, о которой я рассказывал на страницах Хабрахабра некоторое время назад: Дисковое кеширование деревьев ленивых вычислений. Результаты расчетов zencad сохраняет в общем кеше, состояние которого можно отслеживать через интерфейс визуализатора. Используемый хеш-алгоритм sha512 с очешуенной избыточностью исключают возможность коллизий хешключей ленивых объектов (Пространство хэша в 10^74 раз больше количества атомов во вселенной).
Данная модель при создании генерирует четыре мегабайта геометрии и при первом проходе может вычисляться довольно продолжительное время:
Работа с резьбовыми поверхностями вычислительно накладна:
Проблема рефлексии топологии. Метод ближней точки
У OpenScad нет операций взятия фаски или скругления. OpenCascade же их предоставляет. Это очень важные операции, и было бы обидно не взять их на вооружения для ZenCad. Есть и другие операции, требующие указания топологического объекта, например, операция взятия тонкостенной модели в примере с бутылкой OpenCascade. В графической CAD системе мы указываем топологический объект (ребро, грань, вершину) мышкой. При написании скрипта такой возможности у нас нет. Нативный OpenCascade решает задачу рефлексией и использует ее для работы с графическими CAD. Хотя ZenCad поддерживает рефлексию по модели, использование ее в качестве основного инструмента имеет ряд значительных недостатков. Во первых, резко увеличивается уровень знаний, необходимый для использования этих инструментов, ибо вы как минимум должны понимать внутреннее топологическое представление. Во вторых, как только в скрипте появляется if, сразу ломается стройность ленивых алгоритмов, а так же довольно существенно усложняется код модели. В процессе довольно длительного раздумья и экспериментов, я остановился на методе ближней точки. Если кратко, при выполнении топологически зависимых операций программа обходит объект и находит ближний к заданной точке топологический объект из числа входящих в тело. Этот объект считается выбранным. Такое решение более затратно вычислительно, но, благодаря кешированию, показывает себя неплохо. Такой подход применяется для всех операций, зависящих от элементов топологии.
Как уже сказано выше, возможность рефлексии модели, по здравому размышлению, также сохранена, как это уже показано в примере выше (Пример: Фильтрация массива точек).
Маркер Q и Маркер W
Габарит модели бывает бывает сложно прочесть с экрана из-за неочевидности масштаба. Отчасти эту проблему позволяют решить маркеры. Имея интуитивно понятный интерфейс (интуитивней некуда), маркеры сигнализируют о кординатах и показывают дистанцию, чем упрощают анализ правильности геометрии и выбор точек для операций типа фаски/скругления.
Отслеживание обновлений источника
Как и старший брат (OpenScad), ZenCad способен обновлять генерируемую модель при модификации файла источника. В сочетании с системой кеширования, это позволяет довольно комфортно модифицировать скрипт, имея перед глазами практически в реальном времени изменяющееся состояние изделия.
Анимация
На этом достоинства zencad не заканчиваются.
ZenCad (спасибо шустрому ядру opencascade) умеет в реальном времени перерисовывать сцену, что позволяет анимировать 3д модель. Анимация реализуется обыкновенной функцией python и позволяет довольно вольно с собой обращаться. Учитывая, что мы находимся в среде python, zencad оказывается способен визуализировать в виде перемещений моделей данные из внешних источников (Например, с использованием multithreading + tcpsocket). Таким образом zencad может использоваться, например, для полунатурного тестирования робототехнических изделий. О, привет Gazebo!!! Привет ROS!!! Приятно видеть, что вы тоже в зрительном зале. Библиотека кинематики, которая существенно упростила бы построение кинематических цепей роботов манипуляторов, кстати говоря, в разработке.
Анимация на текущий момент всё еще в полуэкспериментальном варианте (особенно в части управления камерой) так что подробно останавливаться на ней не буду.
Экспорт — импорт
На текущий момент поддерживается экспорт и импорт в формате brep, что позволяет интегрироваться с freecad и экспорт в формате stl, что позволяет генерировать модели для 3д печати. Также поддерживается генерация скриншотов. В том числе автоматическая. В частности, скриншоты в онлайн мануале генерируются силами ZenCad в автоматическом режиме.
Текущее состояние
На текущий момент ZenCad еще очень далёк от завершения, и тем не менее вполне работоспособен в той части, в которой готов.
Библиотека доступна в pipy для Debian совместимых осей с версиями языка python3.5, python3.6, python3.7
(Возможно, понадобиться установить qt5-default, из-за каких-то проблем с плагинами у PyQt5)
python3 -m pip install zencad
apt install qt5-default
Запуск gui из командной строки:
python3 -m zencad
Запуск gui из python скрипта:
#!/usr/bin/env python3
import zencad
m = zencad.cube(20)
zencad.disp(m)
zencad.show()
К сожалению, прогресс системы идет не так быстро, как хотелось бы. Еще не реализована большая часть апи двумерной геометрии и апи работы с поверхностями, поддержка экспорта и импорта стандартных форматов, не всегда прозрачна обработка ошибок, не проработано автоматическое тестирование, задуманы и пока не реализованы библиотеки для построения резьбовых соединений и шестереночных изделий. В качестве внешнего редактора, что совершенно ненормально, внезапно захардкожен!!! Sublime Text… Также очень хочется доработать систему с тем, чтобы можно было запустить её под Windows (это требует довольно объёмной экспертно-разведывательной работы).
Тем не менее, сейчас zencad позволяет проектировать довольно сложные 3д модели, создавать модели для 3д печати и даже визуализировать кинематику механизмов.
Ссылки на проект
github: https://github.com/mirmik/zencad, https://github.com/mirmik/servoce
pypi: https://pypi.org/project/zencad/, https://pypi.org/project/pyservoce/
manual
Спасибо за внимание.