[Перевод] Создание массива зеркал на 3D-принтере

e5de5db5b237aea8482da0d48fdd33d8.jpeg

Недавно я сделал предложение руки и сердца одному прекрасному человеку с помощью шестигранной зеркальной штуковины, изображенной на фото. Мы оба большие нёрды, и мне хотелось сделать что-нибудь особенное, поэтому я спроектировал и распечатал на 3D-принтере зеркальный массив, чтобы задать заветный вопрос. Зеркала расположены под таким углом, что прямо перед закатом в нашу 8-ю годовщину они отражают свет заходящего солнца на землю, образуя слова «MARRY ME?»

Поскольку этот проект принес мне бурю положительных эмоций, я, как сторонник открытого кода, решил опубликовать все исходные материалы и написать о его разработке. Код несложно модифицировать для создания других массивов зеркал, проецирующих произвольное изображение на любую фокальную плоскость.

Что потребовалось

  • Python 3 и Jupyter со следующими библиотеками: numpy, matplotlib, numpy-stl, hexy, vpython

  • 3D-принтер:

    • Для этого проекта я купил Creality Ender 3 v2 и в целом доволен им, но подойдет любой FDM принтер с точностью 1 мм;

    • Опционально: лак для волос или специальный клей-спрей для 3D-печати (для защиты от деформации);

  • Любой хороший PLA пластик;

  • 1-дюймовые шестигранные зеркальные плитки;

  • Цианакрилатный клей:

    • Чаще всего «суперклей» относится к этому типу. Вы можете применить любой клей для соединения пластика со стеклом, но важно использовать клей, который не расширяется при застывании, иначе это негативно повлияет на результат. Тонкого слоя цианакрилатного клея достаточно, чтобы зеркала держались; слишком много клея может исказить углы.

  • Солнце. 

Вычисление углов зеркал

Основная идея состоит в том, что массив зеркал образует гексагональную сетку. Каждое зеркало имеет барицентр в определенной точке, и мы хотим, чтобы луч солнечного света отражался от него в пиксель в некотором месте на плоскости. Нам известно, как расположено каждое зеркало, где находится желаемый пиксель и источник света. Исходя из этого, мы можем определить положение зеркал в пространстве, чтобы они отражали свет в соответствующие им пиксели.

Рассмотрим одно зеркало и один целевой пиксель. Центр зеркала имеет определенные (x, y, z) координаты, лежащие на некотором векторе mirror_pos, а центр пикселя лежит на векторе target_pos. Давайте определим векторы u и v так, что:

  • u = mirror_pos — target_pos. Это вектор от центра зеркала к центру пикселя;

  • v = (sin Φ, cosΦsinθ, cosΦcosθ). Это вектор от центра зеркала к источнику света — солнцу;

Здесь мы предполагаем, что угол между солнцем и горизонтом — это θ, а азимутальный угол между центром зеркала и солнцем — это Φ;

Поскольку углы отражения такие же, как и углы падения, нормаль зеркала n является биссектрисой этих углов, поэтому нормаль n можно выразить через векторы u и v:

image-loader.svgimage-loader.svg

Теперь, когда мы знаем как необходимо располагать зеркала, давайте перейдем к созданию 3D-модели рамы для зеркал, которая будет удерживать зеркало в определенной точке с необходимым углом наклона.

Создание 3D-модели

3D-модель — это просто набор вершин и граней. Вершины — это углы многоугольника (список координат (x, y, z)), а грани — это список треугольных фасет, определяемых кортежем из трех вершин. Итак, чтобы создать 3D-модель шестиугольной призмы, необходимо вычислить углы призмы и определить фасеты внешних граней призмы. 

Пластиковая рама зеркальной матрицы, напечатанная на 3D-принтере, состоит из сетки шестиугольных призм. Каждая призма имеет основание (x, y) на плоскости и вектор p, определяемый положением барицентра (7 на рисунке), который перпендикулярен вектору n. Чтобы вычислить углы верхней грани, я пересек призму поверхностью, определяемую векторами p, n. и нормалью нижней грани (правое изображение). Затем я просто встроил в свой код вершины для вычисления граней. 

image-loader.svg

Ещё одна вещь, которую я решил добавить — это боковые грани для выравнивания зеркал. Они представляют собой небольшие выступы на двух гранях призмы, которые намного упрощают процесс приклеивания зеркал. Единственная загвоздка заключается в том, что координаты этих выступов не коллинеарны координатам верхних ребер призмы: два выступа всегда должны образовывать угол в 120 градусов, но при крутом наклоне верхней грани призмы углы могут изменяться. Поэтому для вычисления координат этих выступов, я использовал проекцию оси x на плоскость верхней грани призмы: x'=y * n. Затем я повернул этот вектор относительно нормали с шагом 26 и получил необходимые углы выступов.

image-loader.svg

Создание гексагональной сетки

Имея базу для создания необходимых призм, легко создать гексагональную сетку этих структур, поскольку слияние 3D-моделей можно реализовать с помощью объединения списка вершин и граней. Такой подход может приводить к нежелательной внутренней геометрии, но большая часть ПО для 3D-печати достаточно умное, чтобы игнорировать это при печати.

Я сгенерировал координаты гексагональной сетки, которые определяют центр каждой колонны, затем я сгенерировал список шестигранных призм с соответствующими параметрами для отражения, и, наконец, объединил список призм в один .stl файл. По эстетическим соображениям, я решил разделить призмы зазором в 2 мм, сделав основания немного шире. Пример 3D-модели небольшой сетки с общей точкой фокусировки показан ниже. Структуры для выравнивания выделены красным (обратите внимание на разницы углов призмы и выступов для выравнивания).

image-loader.svg

Оптимальное сопоставление пикселей с зеркалами

Одной из самых интересных задач в этом проекте стал вопрос о том, как оптимально сопоставить зеркала с пикселями, при этом максимально увеличив допустимую погрешность фокусировки изображения (в случае немного неправильного расположения, массив зеркал все равно будет отражать разборчивое изображение).

Массив зеркал будет проецировать солнечные лучи на поверхности и формировать изображение, и, когда структура будет расположена под правильным углом и на правильном фокусном расстоянии, тогда (в теории) вы получите то изображение, которое хотели. Однако если вы отклонитесь от фокусного расстояния или ориентации, то изображение немного деформируется. Этот эффект можно смягчить, сделав лучи как можно более параллельными.

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

image-loader.svg

Итак, мы хотим сопоставить зеркала и пиксели таким образом, чтобы лучи были практически параллельны друг другу. Как нам это сделать? Изначально я выбрал «жадный» подход, перебирая зеркала и назначая им пиксель, минимизирующий угол падения (т.е., минимизация скалярного произведения u * n). Однако такой подход работал не очень хорошо, так как учитывал только скалярное произведение, а не направление отраженного света. Вы сможете увидеть искажения изображения на ранних текстовых массивах в следующем разделе, при создании которых использовался описанный подход (хотя отчасти это связано с плохой адгезией между печатной платформой и материалом).

Окончательный алгоритм был вдохновлен самой структурой гексагональной сетки. Гексагональная сетка радиуса R состоит из n шестиугольников:

image-loader.svg

Так, при R=1 имеем 1 зеркало в центре, на следующих кольцах 6, 12, 18 зеркал и так далее. Чтобы минимизировать пересечение лучей, я вычислил центр масс пикселей, а затем сгруппировал их на основе расстояния до центра масс, где количество пикселей в группе равнялось количеству зеркал в соответствующем кольце. Так, центральный пиксель назначался центральному зеркалу, затем 6 других, ближайших к центру, пикселей сопоставлялись 6 зеркалам во втором кольце и так далее. В каждой группе пиксели сопоставлялись с зеркалами по часовой стрелке, начиная с нижних элементов. Так, внешнее кольцо зеркал соответствовало наиболее отдаленным пикселям, при этом нижняя ячейка соответствовала нижнему пикселю, самая левая ячейка — самому левому пикселю.

image-loader.svg

По итогу, этот алгоритм сопоставления работал довольно хорошо (и мне очень нравятся алгоритмы сопоставления). Вот сравнение лучей, образующих фразу «MARRY ME?»: слева зеркала назначены случайным образом, а справа с использованием алгоритма сопоставления. Отчетливо видно, что во втором случае лучи более параллельны. Это означает, что такая матрица будет «прощать» неточность в размещении, оставляя изображение разборчивым.

image-loader.svg

Также, на этом этапе я обнаружил чуть ли не роковую ошибку для всего проекта. В этот момент было потрачено около 20 часов на печать окончательного зеркального массива, и я осознал, что готовый образец будет отражать зеркальную фразу »? EM YRRAM». До этого я не замечал эту проблему, так как тестовое изображение сердца было симметричным и я не понимал, что получаю зеркальное изображение. К счастью, я вовремя обнаружил проблему, т.к. до заветной даты осталась одна неделя и я бы не успел распечатать исправленную модель. Вывод из этого следующий: использование ПО для визуализации очень важно при дебаге.

Печать рамы

Итак, к этому моменту я разработал end-to-end процесс, в котором при введении набора координат, формирующих желаемое изображение, проекционной плоскости и положения солнца, на выходе формируется .stl модель рамы массива зеркал, которую можно напечатать на принтере. Теперь нам просто нужно все распечатать и приклеить зеркала, не так ли?

image-loader.svg

Я напечатал четыре тестовых массива меньших размеров для проверки перед созданием большой рамы. Каждая рама имела 37 зеркал и проецировала изображение сердца (37 зеркал соответствуют R=3 — наибольшему радиусу, который я мог распечатать одним цельным «куском» с помощью моего Ender3 и его печатной платформы 220 на 220 миллиметров).

image-loader.svg

При печати этих массивов я столкнулся с двумя проблемами. Первой проблемой была деформация массива из-за неполного прилегания к печатной платформе. Это приводило к тому, что из-за натяжения охлаждающихся слоёв уголки слегка приподнимались, что приводило к изменению углов призм и искажению изображения. Я решил эту проблему, снизив температуру стола и нанеся на него немного лака для волос (первобытные знания о 3D-печати…), что сработало как заклинание. 

Второй проблемой стала дилемма о том, как распечатать большую раму, которая не подходила под объем печати моего принтера. Некоторые программы для 3D-печати включают в себя встроенные функции для разрезания больших моделей на части, но я не мог их использовать, поскольку плоские разрезы проходили бы сквозь шестиугольные призмы, что создало бы «гребень» на верхней поверхности, который изменил бы угол:

Около нескольких десятых градусаОколо нескольких десятых градуса

Поэтому я создал несколько кастомных функций для разделения модели на несколько маленьких, которые можно было бы склеить вместе. При таком разделении, швы будут находиться в основании модели, что не повлияет на нормали зеркал. 

Я поигрался с парой различных схем разделения, но в конечном счете остановился на цветочном узоре, показанном ниже. Во-первых, мне понравилось, как выглядит такое разделение, а во-вторых, оно обеспечивало большую прочность всей конструкции из-за отсутствия непрерывного шва, по которому собранный массив мог сломаться пополам.

image-loader.svg

На печать всей модели ушло около недели (я использовал адаптивную высоту слоя, чтобы немного ускорить печать нижнего и среднего слоев, но верхние слои должны быть очень тонкими, чтобы установить зеркала точно под необходимым углом). Я собрал раму с помощью цианакрилатного клея (мне пришлось слегка отшлифовать края последней секции, чтобы она подошла по размеру). Полностью собранная рама показана ниже.

image-loader.svg

Последним этапом стало аккуратное приклеивание всех 196 зеркал к готовой раме. При сборке тестовых массивов, я обнаружил, что цианакрилатный клей при застывании выделяет много паров, которые могут оседать на зеркалах и заставлять их выглядеть запотевшими (они по-прежнему отражают достаточно света, но выглядят хуже, чем могли бы…). Чтобы это предотвратить, я использовал вентилятор, пока приклеивал зеркала, для минимизации воздействия пара.

image-loader.svg

Я закончил монтировать все зеркала в ночь перед предложением, так что, по иронии судьбы, мне не удалось протестировать окончательный результат на закате. К счастью, всё сработало именно так, как я и хотел! И, что ещё немаловажно, темпераментный туман в районе залива Сан-Франциско решил взять выходной.

Это был первый проект, который я сделал при помощи 3D-печати (на самом деле я занялся 3D-печатью исключительно ради этой идеи), а также уникальный опыт воплощения идеи с помощью кода и принципов физики.

image-loader.svg

© Habrahabr.ru