Недавно мне посчастливилось очутиться в пабе с очень творческими и талантливыми людьми. Обсуждение затронуло тему светодиодных свечей и того, как можно создать нечто, под любым углом напоминающее мерцающую свечу. Я предложил дисплей на основе эффекта инерции зрения, но большинство сошлось на том, что для этого понадобится слишком много механики: подшипники, возможно, контактные кольца и так далее.
После беседы я подумал, что если двигатель и батарея будут достаточно маленькими, то вращаться может устройство целиком. На следующий день я заказал несколько печатных плат, поэтому я добавил к заказам простую матрицу светодиодов. Маленькие платы из Китая, по сути, бесплатны, единственное, что важно — это скорость доставки.
Недавно мне достался в неограниченную аренду установщик компонентов (Charmhigh CHM-T36VA). Он был взят конкретно под другой проект, который я рано или поздно напишу, но мои впечатления от него можно сформулировать так: роботы — это будущее. За свою жизнь я потратил немало времени на ручную сборку плат, поэтому автомат, способный собрать плату за считаные секунды — это настоящий подарок богов.
Его недостаток заключается в долгой заправке лент. Для каждого уникального компонента приходится монотонно заправлять ленту, что иногда занимает добрую часть от двадцати минут. Плата, для которой я позаимствовал автомат, содержала 26 уникальных компонентов, поэтому для заправки лент потребовался целый рабочий день.
Однако в этой матрице светодиодов был ровно один компонент, поэтому заправка лент была максимально быстрой. Теперь мы можем выпекать платы с умопомрачительной скоростью!
У меня не было нужного шаблона, поэтому я вытравил его лазером на ацетатной плёнке. Этот проект по-прежнему находился на этапе низких инвестиций, я просто лениво набрасывал идеи. Однако стандартные светодиодные матрицы всегда пригодятся, так что можно сделать их несколько штук.
Некоторые я изготовил из 0603, другие из 0805, потому что они уже были заправлены в автомат.
Если бы мне не нужно было торопиться с проектированием плат, то я бы сделал круглую плату, чтобы крепить её под прямым углом. Площадки вдоль нижней части нужно было припаять напрямую к платам. Когда я начну создавать следующую версию, то так и поступлю.
Пока я просто игрался с идеями. Я знал, что хочу микроконтроллер с достаточно большим объёмом флэш-памяти, потому что мне могут потребоваться объёмные видеоданные. У меня был соблазн выбрать Pico, имеющий двухъядерный процессор с частотой 125 МГц (или больше) и до 16 МБ флэш-памяти (и важно то, что он дешёвый). Один из основных недостатков Pico заключается в том, что сама по себе работа с голым чипом RP2040 очень неудобна. У него нет встроенной флэш-памяти, поэтому вам как минимум придётся подключать флэш-чип QSPI, а ещё почти всегда требуется внешний кристалл и приличное количество вспомогательных конденсаторов. Сама по себе плата Pico слишком велика для нашей ситуации.
Однако многие производители выпускают минимальные платы RP2040 специально под такие случаи. Большинство из них мне не подходили: они были или слишком большими, или имели недостаточно GPIO. Я нашёл одну, показавшуюся мне многообещающей: Waveshare RP2040-tiny. По сути, её производитель разрезал плату Pico пополам, оставив на основной плате самый необходимый минимум и добавив вторичную плату с USB-разъёмом, кнопками сброса и запуска, подключённую плоским гибким кабелем.
Она показалась мне идеальным выбором для моего прототипа. Она всё равно слишком большая и у неё тоже доступны не все GPIO, но с этим мы должны справиться.
В качестве батареи я сразу же выбрал LIR2450. Это перезаряжаемый литий-ионный аккумулятор, выдающий сильно больше 100 мА. Можно достать литий-ионные аккумуляторы меньшего размера, но их ёмкость и допустимая нагрузка по току сильно меньше. Ещё я немного нервничал из-за того, что у меня валялась повсюду куча батарей LIR2032: мне казалось, что я случайно вставлю их куда-то, где нужна CR2032 (имеющая те же физические размеры), и сломаю устройство (при полном заряде батареи LIR выдают 4,2 В). Кроме того, плата RP2040 по диагонали имеет размер примерно 29 мм, поэтому выбор батареи меньшего размера никак не уменьшит готовое устройство.
Я напечатал из PETG минималистичный держатель для аккумулятора.
Здесь я был слишком экономен, печатая с толщиной стенок 0,5 мм. Держатель состоит из двух частей, верхняя приклеивается к нижней. Думаю, будет разумнее сделать все детали более толстыми и распечатать всё за один раз под углом в 90 градусов; позже я попробовал так сделать. Эта деталь, напечатанная на 3D-принтере, определённо является слабым местом: каждый раз, когда я ронял прототип, она ломалась и мне приходилось снова её приклеивать.
Ну а тем временем мы получили свой первый макет:
Это припаянный мной инфракрасный датчик TCRT5000 с крошечными резисторами поверхностного монтажа. Датчик крупноват, но это всё, что у меня было. Вывод у него аналоговый, детектор — это обычный фотодиод, но можно использовать подтягивающий резистор и подключить его напрямую к контакту GPIO. На входах RP2040 стоят триггеры Шмидта (которые можно отключить программно), так что мы, по сути, получаем бесплатный компаратор.
На плате есть светодиод WS2812, соединённый с GPIO16. Мне гораздо больше пригодился бы дополнительный контакт GPIO для моей матрицы, поэтому я просто выломал светодиод и припаял к нему эмалированный провод. Я не был уверен, что это понадобится, но теперь это единственный способ проверить.
Затем я начал припаивать матрицу. Выступы в нижней части матрицы задумывались потому, что в плате должны быть разъёмы, к которым их можно подсоединить, благодаря чему вся конструкция была бы ровной. Но на самом деле они удобны только как небольшие ножки, которые позволяют плате находиться непосредственно над компонентами. Я припаял к площадкам провод с однопроволочной жилой и подключил его так, чтобы всё держалось жёстко. Можно скорректировать угол между платами, но для этого нужны достаточные усилия, поэтому случайно это произойти не может.
Закончив с половиной соединений, нужно припаять сзади ещё десяток проводов, но прежде нужно подключить двигатель.
У меня было несколько двигателей примерно подходящего размера. Я выбрал нужный с маркировкой RF-410CA. Большинство похожих двигателей из приводов CD и DVD имеют немного различающиеся диаметры и длины валов. Также мне нужно было подумать о частоте вращения. У большинства этих двигателей скорость без нагрузки составляет от 5000 до 10000 оборотов в минуту (RPM), что слишком много. Можно уменьшить скорость при помощи ШИМ, но это повлияет и на вращающий момент при запуске. Чтобы получить 30FPS, мне нужно 1800 RPM. В этом аспекте сложно принимать решения, потому что как только двигатель начнёт вращаться, то возникнет сопротивление воздуха и, вероятно, установится какое-то равновесие. Ну да ладно, если двигатель не подойдёт, его можно будет поменять потом.
Также я припаял на своё творение небольшой МОП-транзистор sot-23 и диод обратной цепи.
Инфракрасный светодиод подсоединён напрямую к шине питания. В идеале нам бы хотелось иметь программный контроль над ним, чтобы экономить энергию, пока устройство не вращается, но в этом прототипе я не хотел тратить на это GPIO. Матрица имеет размер 8x10, то есть ей нужно 18 GPIO, плюс один для входа датчика и один для управления двигателем, и ещё я хотел приберечь один для мониторинга напряжения аккумулятора. Думаю, можно подсоединить к плате RP2040-tiny ещё парочку GPIO, которые выводились бы как «контакты выбора пользовательского режима», но пока я в первую очередь не хотел заморачиваться и стремился как можно быстрее превратить это в работающий прототип.
Обратите внимание, что я подключаю матрицу напрямую к контактам GPIO. Нет никакого ограничения по току или транзисторам. Суммарно RP2040 может подавать/погашать на все свои GPIO примерно 50 мА. Я узнал о рискованном, но уж очень привлекательном способе управления светодиодами с микроконтроллеров: отказаться от резисторов ограничения тока и управлять ими при помощи ШИМ. Внутреннего сопротивления во включённом состоянии контакта GPIO достаточно для ограничения тока; возможно, не до уровня, обеспечивающего непрерывное свечение, но вполне предсказуемо. Если не ограничивать рабочий режим, то это нормально. В этом проекте мы будем очень быстро мерцать всей матрицей, тщательно подбирая тайминги. Думаю, маловероятно, что мы перегрузим светодиоды, если только не произойдёт сбой чипа и матрица не зависнет.
Остальную часть матрицы я смонтировал при помощи эмалированного провода, вдевая его снизу. Нам не нужна дополнительная жёсткость; к тому же так всё выглядит довольно красиво.
В голых проводах есть что-то удивительно привлекательное, какая-то киберпанковская аура. Эта аура только усилилась, когда я впервые включил матрицу с шахматным тестовым паттерном.
Я приклеил к двигателю распечатанный держатель аккумулятора, а остальную часть прототипа закрепил гибкой лентой 3M. Затем мне пришлось укоротить провода к двигателю и подключить выводы аккумулятора.
Я подключил положительный вывод аккумулятора напрямую к VBUS платы. RP2040 находится за регулятором 3,3 В, но при подключённом аккумуляторе подсоединение кабеля USB может подать 5 В на выводы аккумулятора. Впрочем, об этом мы можем позаботиться позже. Пока я просто хочу знать, есть ли хоть какой-то шанс на то, что наш прототип заработает.
Как ни удивительно, после того, как я сконфигурировал его на то, чтобы включать матрицу в ответ на инфракрасный светодиод, он зажигал матрицу при срабатывании вспышки камеры.
Сняв аккумулятор, можно увидеть моё подключение к нему: это просто зачищенный и лужёный эмалированный провод, пропущенный через пару небольших отверстий. На этом этапе аккумулятор вставлялся и держался без проблем. Лишь после того, как я несколько раз уронил устройство и пластик треснул, мне понадобилось использовать для его крепления резиновое кольцо.
Сзади я просверлил маленькое отверстие, чтобы можно было доставать аккумулятор, просто толкнув его чем-нибудь острым.
▍ Программное обеспечение
Мы мониторим инфракрасный датчик и используем время между срабатываниями, чтобы установить скорость матричного дисплея. Всё это привычное дело для дисплея на основе эффекта инерции зрения, добавляется лишь ещё одно измерение.
В RP2040 мне нравится то, что можно задавать все контакты GPIO в одном такте синхронизации. Чипы STM32, несмотря на использование 32-битных процессоров, группируют ввод-вывод в 16-битные регистры, страдающие от конфликтов на шине при попытке их одновременного изменения.
Мы можем предварительно обрабатывать всё, что нужно передавать на GPIO, и пошагово отправлять эти данные со скоростью, пропорциональной измеряемому вращению.
На плате установлен двухъядерный процессор ARM Cortex-M0. Стоит отметить, что каждое из ядер имеет отдельное оборудование SysTick. Вместо того, чтобы использовать прерывания, мы можем использовать оба ядра в циклах ожидания освобождения. Первое ядро отслеживает инфракрасный датчик и использует его systick для измерения точного количества тактов между срабатываниями. Второе ядро ждёт сигнала на включение освещения, а после его получения обходит буфер объёмных данных, используя для тактовой точности собственный таймер systick.
Что касается управления двигателем, то я был уверен, что нам не потребуется делать ничего особо сложного. Важно, чтобы устройство вращалось с постоянной скоростью для обеспечения постоянной частоты кадров, но оно должно быть достаточно саморегулируемым. В старых механических устройствах в качестве регуляторов часто использовались колёса с лопастями. Я реализовал простейшую логику управления скоростью: если RPM ниже 1200, установить 90% мощности двигателя, в противном случае — на 60% мощности.
Возможно, потом я дополню систему полноценным ПИД-регулированием, но пока устройству достаточно инерции и сопротивления воздуха, чтобы такое простое управление работало нормально.
Когда мне впервые удалось раскрутить устройство, я был вне себя от восторга. За считаные секунды я смог заставить его рисовать простой объёмный шахматный паттерн. Вот один из самых первых тестов:
План заключался в том, чтобы выточить основание для этого устройства с небольшим указателем, который бы торчал вверх, чтобы инфракрасный датчик мог «смотреть» на него. Но потом я понял, что достаточно держать рядом с ним палец, поэтому так и не добрался до этой части. К двигателю был прикреплён очень маленький ролик, которого как раз было достаточно для его раскручивания без вываливания. Позже я вырезал лазером диск из делрина, который прижатием насадил на вал; это была временная заглушка, пока я не доберусь до токарного станка.
Код обходит матрицу по порядку столбцов. Если посмотреть на устройство сверху вниз, каждая радиальная линия имеет небольшое искривление по спирали, но это гораздо проще компенсировать (если бы нас это заботило), чем превращение всей матрицы в двойную спираль. Рабочий цикл светодиодов в центре пропорционально уменьшен по сравнению со светодиодами на периферии.
Мне быстро удалось заставить дисплей отображать статичный тестовый объём. Было лишь вопросом времени, когда я уроню устройство на пол. 3D-печать треснула по линии склейки.
Но это нас не волнует, просто будем постоянно склеивать всё заново. Устройство будет неубиваемым!
▍ Уровень аккумулятора
Меня беспокоило то, что у нас нет схемы защиты аккумулятора. Если напряжение упадёт сильно ниже 3 В, то он будет неустранимо повреждён. На практике оказывается, что граница ближе к 2,7 В, но это зависит от ячейки. При этом процессе я сломал несколько ячеек. У большинства литиевых аккумуляторов есть встроенные схемы защиты, отключающие их, когда они достигнут опасно низкого уровня. Но с голой ячейкой ситуация другая. По крайней мере, нам нужно мониторить напряжение аккумулятора, чтобы мы знали о нём и он не запускался, если мы думаем, что оно слишком мало.
Обычно для мониторинга уровня напряжения питания добавляют делитель напряжения, преобразующий его в читаемый диапазон аналогово-цифрового преобразователя. Кажется, у плат Pico он обычно подключён к одному из их GPIO, но не у RP2040-tiny. Я добавил к нашему последнему доступному GPIO два резистора 100K для питания и заземления.
Проблема в том, что в Pico нет эталонного напряжения. Можно использовать внешний контакт (не разведённый на RP2040-tiny), но если эталоном для АЦП является напряжение питания, то при его снижении мы не сможем этого распознать. По крайней мере, качественно.
Регулятор малого падения напряжения на 3,3 В имел маркировку и падение на 220 мВ при 300 мА. Это значит, что когда напряжение аккумулятора достигает 3,52 В, напряжение питания RP2040 тоже начнёт падать. Показания АЦП как функция от напряжения аккумулятора будут примерно такими:
Разумеется, напряжение отпускания — это функция от токовой нагрузки, поэтому она не будет столь предсказуемой. В этом прототипе я сделал так, чтобы устройство отображало предупреждение, когда напряжение опустится чуть ниже 3,6 В. Это единственное, что мы можем тут сделать. В следующей версии я добавлю эталонное напряжение. Возможно, даже получится подключить эталонное напряжение АЦП к внутреннему регулятору 1,8 В Pico. Этого было бы достаточно.
▍ Зарядное устройство аккумулятора
Я задумывал, что буду извлекать LIR2450 при низком заряде и вставлять его в отдельное зарядное устройство. При самом первом использовании оно сломалось.
Меня выбесило то, что совершенно новое зарядное устройство так подвело (а оно ведь даже не самое дешёвое!). И это в самый захватывающий момент, когда я уже готов к отображению новых объёмных данных! Горе мне!
Я напечатал новый держатель аккумулятора, а этот раз в другой плоскости. Прижимающие выступы гнутся в самом слабом направлении, но они достаточно прочные и толщина стенок теперь везде равна 1 мм.
Я не планировал использовать его для дисплея, это просто моё новое зарядное устройство аккумулятора. Я ограничил источник питания током 50 мА и установил постоянное напряжение 4,2 В. Этого достаточно для зарядки одной литий-ионной ячейки. Постоянный ток выставлен консервативно, я не был уверен, какой это аккумулятор, на 120 мА*ч или на 60 мА*ч. Лучше не выполнять зарядку быстрее, чем примерно 1C, если только на аккумуляторе не написано иное. Но обычно при зарядке медленнее, чем 1C, проблем никогда не возникает.
Это снова пробудило мой энтузиазм и эксперименты продолжились. Но прежде чем мы продолжим рассказ, добавлю, что снятие аккумулятора и его подключение к источнику питания — самый неудобный способ зарядки устройства, особенно после того, как прототип треснул и мне пришлось добавить резиновое кольцо, чтобы аккумулятор не вылетал.
Плата USB-адаптера RP2040-tiny по-прежнему использовалась для загрузки кода в чип. Если мы создадим своего рода плату перехвата USB, то сможем поднять линию на 5 В и вывести контакты для аккумулятора. Он находится между USB-кабелем от PC к плате программирования RP2040-tiny.
Теперь мы можем подключать источник питания к аккумулятору, не вынимая его из прототипа. Линии данных по-прежнему подключены внизу, поэтому мы можем программировать плату и с установленным аккумулятором.
Я осознал, что припаивание было пустой тратой времени, потому что я просто мог поместить провода на плату адаптера RP2040-tiny.
Однако в диком восторге, вызванном работающим объёмным дисплеем, и в расстройстве, что у меня есть в запасе только один LIR2450, я понял, что это всё равно слишком неудобно для быстрой разработки. Где-то в ящике у меня валялись интегральные схемы зарядки литий-ионных аккумуляторов. Мне сразу удалось их найти. Они довольно качественные и стоили примерно по 90 пенсов за штуку. Их артикул BQ21040DBVR. Я вернулся к плате перехвата USB и подключил интегральную схему зарядки прямо в её середину.
Благодаря этому можно оставлять кабель программирования подключённым, и он будет заряжать аккумулятор, пока мы размышляем.
Разумеется, он никогда не зарядит таким образом аккумулятор полностью, потому что прототип никогда не выключается. Один только инфракрасный светодиод постоянно потребляет около 9 мА. На плате адаптера RP2040-tiny есть очень яркий светодиод питания, я поставил резистор на 20K, чтобы он перестал тратить энергию. Но всё равно, я думаю, что даже когда прототип не работает, он суммарно потребляет примерно 15 мА. Зарядная интегральная схема в фазе постоянного напряжения ждёт, пока зарядный ток упадёт ниже 0,1C. Так как у нас зарядный ток установлен на 54 мА, этого никогда не произойдёт. Кроме того, учитывая падение напряжения в кабеле, аккумулятор, вероятно, не получает больше 4,1 В. Всё это не очень важно, просто стоит запомнить для следующей версии.
И наконец мы можем начать заниматься симуляцией жидкостей!
▍ Генерация объёмных данных
Нам нужно создавать объёмные данные в трёхмерных полярных координатах, то есть r, theta и z.
Я начал с каркаса кубика, который хотя бы должен быть узнаваем. Я намеренно вращал его вертикально, чтобы он отображался максимально неуклюже. На самом деле, я думаю, следует написать процедуру векторного дисплея для устройства, которая бы выполняла интерполяцию Брезенхэма для трёхмерных полярных координат. Есть много информации о том, как применять Брезенхэма к 3D и рисованию окружностей, но мы хотим рисовать в полярных координатах прямые линии. Мне кажется, об этом интересно было бы порассуждать, но пока давайте сосредоточимся на экспорте полярных воксельных данных из Blender.
Это стандартный куб с модификатором Wireframe. Чтобы вращать куб вертикально и чтобы угол при этом был направлен строго вверх, нам нужно сначала повернуть x на 45 градусов, а затем повернуть y на atan(1/sqrt(2))
. Одна из удобных особенностей Blender заключается в том, что можно просто вводить в поля формулы.
Чтобы получить срезы этого каркасного куба, я добавил ещё один куб, изменил его форму так, чтобы он напоминал срез и применил к ним модификатор Boolean. Затем я сделал родительским элементом камеры и этого среза пустой объект, а затем анимировал вращение по оси Z этого пустого объекта.
Я переключил камеру на ортографическую проекцию, а разрешение сменил на 8x10. Фон сделал чёрным, а материал куба — излучающим свет (emissive). В редакторе узлов можно использовать colour ramp для задания пороговых значений. Объёмный дисплей пока имеет глубину цвета в 1 бит, поэтому каждый воксель или включён, или выключен. Пороговые значения позволяют нам визуально выбирать наилучшее значение отсечения.
«Render animation» теперь генерирует 24 изображения, 24 среза каркасного куба. Я написал небольшой скрипт на Python, чтобы записать их в файл заголовка, который можно включить в код.
В Blender не только можно вводить в поля формулы и не только почти все их превращать в ключевые кадры, но и настроить драйвер. Вместо того, чтобы вычислять формулу при вводе, он пересчитывает результат для каждого кадра. Поэтому вместо ручного создания ключевых кадров камеры и ручного их переключения в линейную интерполяцию я просто ввёл (frame/24)*2*pi
, которое бесконечно изменяется в цикле. Для вращения куба по оси y я ввёл floor(frame/24)*pi/24
, чтобы он немного вращался на каждый полный цикл камеры.
Честно говоря, было бы вполне нормально, если бы вращение было плавным, но я хотел, чтобы каждый кадр данных был дискретным просто на случай, если захочу регулировать скорость воспроизведения на основании частоты оборотов двигателя.
Можете лишь поверить мне на слово, что этот дисплей в реальной жизни выглядит более трёхмерным. При просмотре изображений и видео просто кажется, что горит куча случайных точек.
▍ Симуляции жидкостей
Выполнение симуляции жидкости в Blender — это и простая, и сложная задача. Начать легко, сделать всё правильно — сложно. В деле участвует огромное множество параметров.
Симуляцию жидкости чуть проще портировать на объёмный дисплей, поскольку можно запросто преобразовать частицы жидкости в меш. Теоретически, у нас должно получиться выполнять симуляцию жидкости на скорости 1/24 и использовать ту же технику, что и раньше, для извлечения полярных объёмных данных.
К сожалению, использование крайних значений параметров, например, очень медленный масштаб времени, приводит к нестабильностям. Похоже, не существует простого способа воспроизведения симуляции с пониженной частотой, а очень медленная скорость симуляции при ускорении выглядит совершенно неестественно. Я довольно долго экспериментировал, но без особого успеха. С другой стороны, мы создаём очень низкое пространственное разрешение, поэтому симуляции жидкости достаточно быстры, чтобы работать на моей десктопной машине в реальном времени.
Я изучил и другие способы рендеринга объёмных данных. Существует функция Multi-view, или Stereoscopy, предназначенная для рендеринга 3D-видео. Она позволяет добавить две камеры и одновременно рендерить сцену с двух точек. Можно добавить любое количество камер, а результаты рендеринга различаются по суффиксу имени файла. Не знаю, существует ли быстрый способ добавить 24 камеры и равномерно их вращать (к сожалению, к камере нельзя применить модификатор Array; возможно, до меня это никому не требовалось), но при такой стратегии возникает ещё одна проблема: нам также нужно, чтобы модификаторы Boolean наших срезов рендерились одновременно.
Вместо использования модификатора Boolean и среза можно немного сжульничать и применить встроенные расстояния усечения (clipping distance). Настроив камеру на рендеринг лишь 0,1 среза сцены, мы получим почти правильный результат. Проблема в том, что отрисовываются только поверхности, но не сплошная заливка усечённых объектов. Я подумал, что если применить к объектам объёмный материал, возможно, удастся их сделать хотя бы частично заполненными, но немного поэкспериментировав, ничего не добился.
Вместо этого используем более общий (но и более сложный) подход: просто напишем скрипт на Python. Во вкладке скриптинга Blender я написал следующее:
import bpy
import os
from math import pi
obj = bpy.data.objects['Empty']
output_path = bpy.context.scene.render.filepath
for i in range(24):
obj.rotation_euler[2] = (i/24)*2*pi
bpy.context.scene.render.filepath = os.path.join(output_path, f"###_{i:02}.png")
bpy.ops.render.render(animation=True)
bpy.context.scene.render.filepath = output_path
Таким образом мы выполняем симуляцию жидкости в реальном времени и просто повторно рендерим всю анимацию 24 раза с различным поворотом пустого объекта (который является родительским для камеры и среза).
Проверив концепцию, я приступил к симуляции огня. По сути, её рендеринг выполняется так же, только с парой дополнительных этапов. Мы настраиваем симуляцию огня, а затем запекаем её в формат OpenVDB. На изображении я поджёг небольшой куб.
Затем начинаем заново и импортируем данные OpenVDB обратно в Blender. Затем можно создать новый меш и применить к нему модификатор Volume to Mesh, что позволит установить пороговое значение для объёмных данных. Наконец применим ещё один модификатор Boolean со срезом и повторно выполним приведённый выше скрипт.
Наверно, фотография снова не передала нужное ощущение, но, надеюсь, смысл понятен.
Я осознал, что расположение светодиодов можно скомпенсировать в ПО, если бы оно было предсказуемым. Мы можем смещать срез Boolean ближе или дальше от камеры, чтобы он не вращался вокруг точного центра. Если он совпадёт с движением с реального дисплея, это будет идеально. Аналогично, вместо растянутого куба можно придать ему слегка изогнутую форму, чтобы компенсировать паттерн сканирования матрицы при вращении платы. Однако на таком уровне разрешения я не думаю, что подобные улучшения будут заметны.
На самом деле важно лишь то, чтобы свечение отдельного вокселя рядом с периметром выглядело как одна точка, а не как двойная точка под некоторыми углами. Это можно увидеть на изображении ниже, где ближайший к камере воксель вытянут, потому что два свечения при вращении матрицы не совсем согласованы:
Буква «m» в центре идеально чёткая, потому что я здесь намеренно сжульничал. Чтобы текст был читаем со всех направлений, воксели текста рендерятся иным образом. Я сделал так, чтобы текст скроллился в читаемой ориентации, вне зависимости от того, смотрите ли вы на переднюю или заднюю часть дисплея. Как бы то ни было, это несоответствие вокселей можно заметить лишь на периферии дисплея, где два свечения видимы одновременно.
▍ Заключение
Над симуляцией огня предстоит ещё много поработать, но, вероятно, я отложу это до выпуска следующего прототипа, который может быть чуть более ровным и с более высоким разрешением.
Если бы у меня был в запасе небольшой выключатель, то я бы добавил его в этот прототип, чтобы отключать аккумулятор, не снимая его. Я начал искать в ящиках что-нибудь подходящее, но потом понял, что могу просто вставить небольшой кусок плёнки между аккумулятором и контактом, как это делают с ячейками-таблетками в инфракрасных пультах и других устройствах. Сработало вполне неплохо.
Кстати, об инфракрасных пультах дистанционного управления: было бы неплохо сделать ДУ для устройства. У нас уже есть инфракрасный датчик, хотя и не демодулирующего типа. Как видно в ролике, я просто перехожу к следующему режиму, если в течение определённого времени не было действий.
Вот несколько гламурных снимков устройства.
Разумеется, инфракрасное излучение совершенно невидимо невооружённым глазом, но цифровая камера фиксирует его как слабое фиолетовое сияние.
Как обычно, исходный код я выложил на Github.
▍ Видеодемо
Естественно, камера не может передать ощущение объёмности. В реальной жизни дисплей выглядит более трёхмерным.
Автор:
ru_vds