Скриптовый (script) 3D редактор OpenSCAD
Введение
В настоящие время существует большой выбор мощных, универсальных 3D-редакторов для создания и анимирования 3D-сцен, которые в подавляющим случае можно отнести к программам с графическим интерфейсом. Такой подход к созданию 3D сцен имеет одновременно как большое преимуществом так и серьезный недостаток, заключающийся в том, что с одной стороны вам доступны сотни различных объектов и манипуляций над ним, а с другой стороны для качественной работы их все нужно знать и использовать, что требует весьма длительного времени на обучение.
В основу 3D-script редактора OpenSCAD положена абсолютно обратная парадигма, в данном редакторе полностью отсутствует какой либо графический интерфейс для создания 3D-объектов, нет ни одной «кнопки» или пункта «меню» при помощи которого вы могли бы создать какой либо графический примитив и произвести над ним какую либо манипуляцию. Создание всех объектов в OpenSCAD и манипуляции над ними происходят только посредством заранее подготовленного script-кода.
Предлагаю Вам самим ознакомиться с представленным ниже руководством и самому решить оправдан либо такой подход к созданию и анимации 3D-сцен.
Вы можете скачать OpenSCAD, либо воспользоваться его ONLINE версией, но учтите что она немного отличаются в синтаксисе от представленных ниже примеров.
Инструкция по скачиванию, установки и настройки программы не прилагается, подразумевается что если вы решили овладеть OpenScad, то значит вы достаточно технически грамотны чтобы разобраться в таких вопросах самостоятельно.
Данное пособие рассчитано на читателей минимально знакомых с синтаксисом си-подобных языков.
1. Графические примитивы
Напишим две строки кода первой сцены/программы:
$fn = 32;
sphere();
Запустим их на исполнение и получим картинку примерно следующего вида:
Очевидно что строка-2
отвечает за создание сферы, а поскольку никакие параметры, при создании сферы, не были явно указаны, она была создана с параметрами по умолчанию.
Значение первой строки это специальная переменная (Special variables
), о чем свидетельствует символ амперсанда, определяющая на сколько секторов будет разбита любая окружность. Как видно мы установили ее значение равным 32 для большей визуальной гладкости криволинейных поверхностей.
Не стоит беспокоиться о необходимости запоминания сотен специальных переменных, их всего 10 штук, следующих типов:
$fa, $fs, $fn
гладкость криволинейных поверхностей;$t
время;$vpr, $vpt,, $vpd,, $vpf
место расположения и ориентация камеры;$children
номер «наследника/последователя»;$preview
выбор кнопки рендера
Напишим новый код и запустим его:
$fn = 32;
cylinder();
Как можно заметить поведение цилиндра более сложно чем сферы, во первых он считает своей осью симметрии исключительноось-Z
, а во вторых располагает одну из своих торцевых поверхностей в плоскости-XY
с центром в начале координат.
Задействуем теперь все доступные параметры примитива, которых не так и много, чтобы увидить все доступные модификации по умолчанию:
$fn = 32;
cylinder(h = 3, d1 = 2, d2 = 1, center = true);
Как можно заметить, цилиндр поддерживает крайне небольшое количество модификаторов параметров, среди которых даже нету модификатора координат.
Для полноты картины расмотрим третий графический примитив cube()
.
$fn = 32;
cube([3,2,1], center = true)
Кроме выше указанных графических примитивов существуют еще несколько графических примитивов 3D и 2D, расматривать которые в данной статье мы не будем.
2. Манипуляторы примитивы
Добавте в код манипулятор translate()
и самостоятельно измените его параметры отслеживая изменения. Обратите внимание что параметры должны быть не только переданы модификатору, посредством размещения внутри оператора()
, но и быль заключены в оператор[]
который преобразует три одиночных числа x,y,z
в вектор вида [x,y,z]
, принимаемый на входе модификатор translate()
. Если нарушить данное правило, то модификатор translate()
расширит первое одиночное число X
до вектора [x,0,0]
.
$fn = 32;
translate([3,2,1])
cylinder(h = 3, d1 = 2, d2 = 1, center = true);
Обратите внимание, что после модификатора translate()
не нужно указывать оператор;
это вызвано тем, что модификатор оказывает воздействие на первый же примитив следующий за ним, если же за модификатором будет следовать следующий модификатор, то их общее действие сложиться
и будет применено к первому же примитиву, следующему за цепочкой модификаторов.
Добавим в код второй манитулятор rotate()
, и попробуем две различных комбинации их сочетания:
$fn = 32;
rotate([90, 0, 0])
translate([3,2,1])
cylinder(h = 3, d1 = 2, d2 = 1, center = true);
translate([3,2,1])
rotate([90, 0, 0])
cylinder(h = 3, d1 = 2, d2 = 1, center = true);
Как видно, два абсолютно одинаковых модификатора, но в разном последовательности оказывают различное воздействие на графический примитив. Это естественное следствие векторных вычислений, где в отличии от скалярного, результат меняется в зависимости от перестановки слагаемых.
Поэкспериментируйте с параметрами и сочетаниями модификаторов самостоятельно, чтобы понять как и в какой последовательности они воздействуют друг на друга.
3. Составные объекты (модули)
Напишем код создающий объект состоящий из трех примитивов, моделирующий стержень круглого сечения и переменного диаметра.
$fn = 32;
cylinder(h = 3, d = 2);
translate([0, 0, 3])
cylinder(h = 2, d1 = 2, d2 = 1);
translate([0, 0, 3 + 2])
cylinder(h = 1, d = 1);
Обратите внимание что поскольку действие модификаторов распространяется только на примитив который следует за ним и заканчивается при достижении ;
. в строке-5
было необходимо сложить высоту первого и второго цилиндра, для того чтобы третий оказался выше них обоих.
Изменим код чтобы продемонстрировать как можно распространить действие одного модификатора на несколько примитивов.
$fn = 32;
cylinder(h = 3, d = 2);
translate([0, 0, 3]) {
cylinder(h = 2, d1 = 2, d2 = 1);
translate([0, 0, 2])
cylinder(h = 1, d = 1);
};
Заключение группы модификаторов и примитивов в один блок, посредством фигурных скобок, позволяет оперировать ими как одним целым.
Введем в код символьные константы, для придания ему большей читабельности и облегчения его дальнейшего совершенствования.
Стоит отметить что хотя в англоязычном руководстве символьные константы называются переменными, фактически это константы значение которым можно присвоить только при инициализации.
$fn = 32;
diam1 = 2;
diam2 = 1;
heig1 = 3;
heig2 = 2;
heig3 = 1;
cylinder(h = heig1, d = diam1);
translate([0, 0, heig1]) {
cylinder(h = heig2, d1 = diam1, d2 = diam2);
translate([0, 0, heig2])
cylinder(h = heig3, d = diam2);
};
Прорисовка объекта совершенно не изменилась, но зато теперь мы можем изменять его весь целиком меняя значения всего лишь нескольких констант.
Инкапсулируем фигуру, сделав из нее «шаблон», для будущего применения изменив код следующим образом.
$fn = 32;
module Arm(diam1 = 2, diam2 = 1, heig1 = 3, heig2 = 2, heig3 = 1) {
cylinder(h = heig1, d = diam1);
translate([0, 0, heig1]) {
cylinder(h = heig2, d1 = diam1, d2 = diam2);
translate([0, 0, heig2])
cylinder(h = heig3, d = diam2);
};
};
Как можно заметить после этих изменений объект пропал. Это произошло потому что теперь код лишь «описывает» объект, но не создает его, добавим в самый конец файла еще одну строку и повторим рендер.
Arm();
Стоит заметить что мы не только определили символические имена констант, но и присвоили им значения по умолчанию, что позволяет нам создавать объект с параметрами по умолчанию.
4. Проверка типа параметра
Напишем код для создания стержня который может опционально заканчиваться «втулкой», внесем изменение в код в два этапа, для реализации указанного функционала.
$fn = 32;
module Arm(list = [[1,2],[1,2,3]]) {
echo(Arm = list);
diam = list[0];
heig = list[1];
cylinder(h = heig[0], d = diam[0]);
translate([0, 0, heig[0]]) {
cylinder(h = heig[1], d1 = diam[0], d2 = diam[1]);
translate([0, 0, heig[1]])
cylinder(h = heig[2], d = diam[1]);
};
};
Arm();
В указанном коде количество передаваемых параметров сократилось до одного, которому присвоенно символическое имя list
, но одновременно указанный параметр представлляет собой сложным
вектором, состоящим из двух простых
векторов.
В начала модуля мы раскладываем сложный
вектор на два простых
, с символическими более простыми и понятными именами diam
, heig
, и осуществляем все дальнейщие посроения и манипуляции уже посредством этих векторов.
Под термином сложный
/простой
подразумевается содержит ли вектор в себе подвекторы или состоит только из чисел.
Особо стоит отметить модификатор echo
, как несложно догадаться он выводит в консоль содержимое вектора list
. Сейчас значение list
известно нам заранее, но в будущем при отладке более сложного кода всегда полезно видеть входные параметры.
Найдите консоль и посмотрите что было переданно.
Напишем дополнительные элементы кода для реализующими требуемого функционала.
$fn = 32;
module Arm(list = [[[1,2],[1,2,3]], 3, 2]) {
echo(Arm = list);
diam = list[0][0];
heig = list[0][1];
fork = [list[1], list[2]];
if (is_num(fork[0]))
rotate([0, 90])
cylinder(h = fork[0], d = diam[0], center = true);
cylinder(h = heig[0], d = diam[0]);
translate([0, 0, heig[0]]) {
cylinder(h = heig[1], d1 = diam[0], d2 = diam[1]);
translate([0, 0, heig[1]]) {
cylinder(h = heig[2], d = diam[1]);
if (is_num(fork[1]))
translate([0, 0, heig[2]])
rotate([0, 90])
cylinder(h = fork[1], d = diam[2], center = true);
};
};
};
Arm();
В строке-9
и строке-19
, посредством оператора is_num
мы проводим проверку определен ли параметр фактически и является ли он числом, и если параметр определен производим отрисовку дополнительных элементов объекта.
5. Рекурсивный вызов и передача объектов по вектору
Напишем код для создания стерженя состоящего из пяти участков и имеющий три различных диаметра. На первый взгляд имеющийся на данный момент код в принципе не соответствует такой задаче и нужно написать новый, но давайте не будем выбрасывать существующий код, а попробуем его усовершенствовать.
$fn = 32;
module Arm(list = [[[1,2],[1,2,3],[1,[2,1],undef]], undef, 2]) {
echo( Arm = list);
diam = list[0][0];
heig = list[0][1];
next = list[0][2];
fork = [list[1], list[2]];
if (is_num(fork[0]))
rotate([0, 90])
cylinder(h = fork[0], d = diam[0], center = true);
cylinder(h = heig[0], d = diam[0]);
translate([0, 0, heig[0]]) {
cylinder(h = heig[1], d1 = diam[0], d2 = diam[1]);
translate([0, 0, heig[1]])
if (is_list(next)) {
diam = [diam[1],next[0]];
heig = [heig[2],next[1][0],next[1][1]];
next = next[2];
Arm([[diam,heig,next],undef,fork[1]]);
}
else {
cylinder(h = heig[2], d = diam[1]);
if (is_num(fork[1]))
translate([0, 0, heig[2]])
rotate([0, 90])
cylinder(h = fork[1], d = diam[1], center = true);
};
};
};
Arm();
Применив в строке-23
рекурсивный вызов модуля Arm
можно создавать объекты состояший из неограниченного числа однотипных блоков.
6. Вложенные модули и сокрытие имен
Напишем код для создания плоской закругленной пластины Slab
, с возможностью рекурсивного вызова для добавления дополнительных звений. Поскольку код Slab
аналогичен коду Arm
, предлагаю читателю разобраться с ним самостоятельно.
$fn = 32;
module Slab (list = [3,1,.5,[30,30],[2,30,undef]]) {
echo(Slab = list);
length = list[0];
diameter = list[1];
thickness = list[2];
angle = is_list(list[3]) ? list[3]:
is_num(list[3]) ? [0, list[3]]: [0,0];
next = list[4];
rotate([0, 0, angle[0]]) {
cylinder(h = thickness, d = diameter, center = true);
translate([length / 2, 0, 0])
cube([length, diameter, thickness], center = true);
translate([length, 0, 0])
if (is_list(next))
rotate([0, 0, angle[1]])
Slab([next[0], diameter, thickness, next[1], next[2]]);
else
cylinder(h = thickness, d = diameter, center = true);
};
};
Slab();
Стоит обратить внимание что значение константы angle
, определяемое в строках-8,9
, зависит от того являеться ли переданный параметр вектором.
Напишем теперь код создающий две параллельных пластинки Fork
, которые в последствии применим в Arm
.
module Fork (list = [1,[1,1,.5,[30,30],[2,30,[1,-30]]]]) {
echo(Fork = list);
gap = list[0];
thickness = list[1][2];
slab = list[1];
translate([0, 0, (thickness + gap) / 2])
Slab(slab);
translate([0, 0, -(thickness + gap) / 2])
Slab(slab);
};
Fork();
Как легко заметить для создания модуля Fork
, необходим модуль Slab
. При этом модуль Slab
имеет глобальную видимость и доступен для всех прочих модулей, что может создать конфликт имен. Инкапсулируем модуль Slab
внутри модуля Fork
.
$fn = 32;
module Fork (list = [1,[1,1,.5,[30,30],[2,30,[1,-30]]]]) {
echo(Fork = list);
gap = list[0];
thickness = list[1][2];
slab = list[1];
translate([0, 0, (thickness + gap) / 2])
Slab(slab);
translate([0, 0, -(thickness + gap) / 2])
Slab(slab);
module Slab (list = [3,1,.5,[30,30],[2,30,undef]]) {
echo(Slab = list);
length = list[0];
diameter = list[1];
thickness = list[2];
angle = is_list(list[3]) ? list[3]:
is_num(list[3]) ? [0, list[3]]: [0,0];
next = list[4];
rotate([0, 0, angle[0]]) {
cylinder(h = thickness, d = diameter, center = true);
translate([length / 2, 0, 0])
cube([length, diameter, thickness], center = true);
translate([length, 0, 0])
if (is_list(next))
rotate([0, 0, angle[1]])
Slab([next[0], diameter, thickness, next[1], next[2]]);
else
cylinder(h = thickness, d = diameter, center = true);
};
};
};
Fork();
Теперь попытка создания модуля Slab
за пределами модуля Fork
будет приводить к ошибке.
По умолчанию все символические имена существуют только в пределах объявленного блока, а при совпадении имен более «нижний» уровень перекрывает более «высокий».
7. Подключение дополнительных файлов
Сохраним код содержащий определение Fork
в отдельном файле с именем Fork.scad
Напишем код который позволит нам присоединить к «верхнему» концу Arm
вилку Fork
применив директиву include
.
$fn = 32;
module Arm(list = [[[1,2],[1,2,3],[1,[2,1],undef]],
undef,
[1,[1,1,.5,[-30,30],[1,30,[1,-30,undef]]]]]) {
echo( Arm = list);
diam = list[0][0];
heig = list[0][1];
next = list[0][2];
fork = [list[1], list[2]];
if (is_num(fork[0]))
rotate([0, 90])
cylinder(h = fork[0], d = diam[0], center = true);
cylinder(h = heig[0], d = diam[0]);
translate([0, 0, heig[0]]) {
cylinder(h = heig[1], d1 = diam[0], d2 = diam[1]);
translate([0, 0, heig[1]])
if (is_list(next)) {
diam = [diam[1],next[0]];
heig = [heig[2],next[1][0],next[1][1]];
next = next[2];
Arm([[diam,heig,next],undef,fork[1]]);
}
else {
cylinder(h = heig[2], d = diam[1]);
if (!is_undef(fork[1]))
translate([0, 0, heig[2]])
rotate([0, -90])
if (is_list(fork[1])) {
heig = fork[1][0];
cylinder(h = heig, d = diam[1], center = true);
Fork(fork[1]);
}
else
cylinder(h = diam[1], d = diam[1], center = true);
};
};
include;
};
Arm();
8. Наследование
Предположим необходимо создать двухзвенный манипулятор, текущая версия модуля Arm
вполне работоспособна и позволяет создать два звена манипулятора, но она не позволяет объединить их в один объект посредством какой либо простой манипуляции.
При попытки создать два звена манипулятора, они будут сгенерированы каждый сам по себе и совершенно не будут учитывать факт существования друг друга, а также какой либо иерархии, когда одно звено должно быть главным и способным перемещаться самостоятельно, а второе зависимым и учитывать перемещение главного.
Реализуем требуемый функционал посредством технологии наследования
реализованной в операторе children
, добавив его в существующий код.
$fn = 32;
module Arm(list = [[[1,2],[1,2,3],[1,[2,1],undef]],
1,
[1,[1,1,.5,[-30,30],[2,30,[1,-30]]]]]) {
echo( Arm = list);
diam = list[0][0];
heig = list[0][1];
next = list[0][2];
fork = [list[1], list[2]];
if (is_num(fork[0]))
rotate([0, 90])
cylinder(h = fork[0], d = diam[0], center = true);
cylinder(h = heig[0], d = diam[0]);
translate([0, 0, heig[0]]) {
cylinder(h = heig[1], d1 = diam[0], d2 = diam[1]);
translate([0, 0, heig[1]])
if (is_list(next)) {
diam = [diam[1],next[0]];
heig = [heig[2],next[1][0],next[1][1]];
next = next[2];
Arm([[diam,heig,next],undef,fork[1]])
children();
}
else {
cylinder(h = heig[2], d = diam[1]);
translate([0, 0, heig[2]])
if (is_undef(fork[1]))
children();
else {
if (is_list(fork[1])) {
heig = fork[1][0];
rotate([0, -90])
cylinder(h = heig, d = diam[1], center = true);
Fork(fork[1])
children();
}
else {
rotate([0, -90])
cylinder(h = diam[1], d = diam[1], center = true);
children();
};
};
};
};
include;
};
Arm() Arm();
Добавление в оператора children()
позволила разместить второе звено манипулятора на месте второй, зависимой
оси первого манипулятора.
Впрочем полученный код не полон, в случае попытки добавить манипулятору вилку, не важно на каком конце, второе звено манипулятора разместиться не на конце вилки, а на оси втулки. Так происходит потому что код вилки не имеет функционал наследования, добавим его и модернизируем код Arm
для поддержки нового функционала.
$fn = 32;
module Fork (list = [1,[1,1,.5,[30,30],[2,30,[1,-30]]]]) {
echo(Fork = list);
gap = list[0];
thickness = list[1][2];
slab = list[1];
rotate([0, -90]) {
translate([0, 0, (thickness + gap) / 2])
Slab(slab);
translate([0, 0, -(thickness + gap) / 2])
Slab(slab)
translate([0, 0, (thickness + gap) / 2])
rotate([0, 90])
children();
};
module Slab (list = [3,1,.5,[30,30],[2,30,undef]]) {
echo(Slab = list);
length = list[0];
diameter = list[1];
thickness = list[2];
angle = is_list(list[3]) ? list[3]:
is_num(list[3]) ? [0, list[3]]: [0,0];
next = list[4];
rotate([0, 0, angle[0]]) {
cylinder(h = thickness, d = diameter, center = true);
translate([length / 2, 0, 0])
cube([length, diameter, thickness], center = true);
translate([length, 0, 0]) {
if (is_list(next)) {
rotate([0, 0, angle[1]])
Slab([next[0], diameter, thickness, next[1], next[2]])
children();
}
else {
cylinder(h = thickness, d = diameter, center = true);
children();
};
};
};
};
};
0. Анимация
Это маленькая затравка для подогрева интереса к следующему посту. Замените последнию строку кода в файл Arm.scad
, создающию два объекта Arm
, на строку указанную ниже и запустите анимацию.
rotate([$t * 360]) Arm() rotate([$t * 360]) Arm();
Послесловие
Данный пост абсолютно не претендует на полное и сичерпывающие описание OpenSCAD, пишите вопросы в комментариях и я постораюсь дать ответы. Небольшая шпаргалка для самостоятельного изучения.