Введение
В настоящие время существует большой выбор мощных, универсальных 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<Fork.scad>;
};
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<Fork.scad>;
};
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, пишите вопросы в комментариях и я постораюсь дать ответы. Небольшая шпаргалка для самостоятельного изучения.
