Введение

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