Трехмерная графика на STM32F103 +70





image

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

В далеком 2017 году (судя по дате модификации файла) решил перейти я с контроллеров AVR на более мощные STM32. Естественно, первым контроллером был выбран широко распиаренный F103. Не менее естественно, что использование готовых отладочных плат было отвергнуто в пользу изготовления своей с нуля согласно своим требованиям. Как ни странно, обошлось почти без косяков (разве что UART1 стоило бы вывести на нормальный разъем, а не костылить на проводках).

По сравнению с AVR характеристики камня довольно приличные: 72 МГц тактовой (на практике можно разогнать до 100 МГц, а то и больше, но только на свой страх и риск!), 20 кБ оперативки и 64 кБ флеша. Плюс тонна периферии, при использовании которой главная проблема — не испугаться этого изобилия и осознать, что для запуска не нужно лопатить все десять регистров, достаточно выставить три бита в нужных. По крайней мере до тех пор, пока не захотите странного.

Когда прошла первая эйфория от обладания такой мощью, возникло желание прощупать ее пределы. В качестве эффектного примера я выбрал расчет трехмерной графики со всеми этими матрицами, освещением, полигональными моделями и Z-буфером с выводом на дисплей 320х240 на контроллере ili9341. Две самые очевидные проблемы, которые предстоит решить — скорость и объем. Размер экрана 320х240 при 16 битах на цвет дает 150 кБ на один кадр. А всего оперативки у нас всего 20 кБ… И эти 150 кБ надо передавать на дисплей хотя бы 10 раз в секунду, то есть скорость обмена должна составлять хотя бы 1.5 МБ/с или 12 Мб/с, что уже выглядит существенной нагрузкой на ядро. К счастью, в данном контроллере присутствует модуль ПДП (прямой доступ к памяти, он же Direct Memory Access, DMA), который позволяет не нагружать ядро операциями переливания из пустого в порожнее. То есть можно подготовить буфер, сказать модулю «вот тебе буфер данных, работай!», а в это время готовить данные для следующей передачи. А если учесть способность дисплея принимать данные потоком, вырисовывается следующий алгоритм: выделяется передний буфер, из которого DMA передает данные на дисплей, задний буфер, в который происходит рендеринг, и Z-буфер, используемый для отсечения по глубине. Буферы представляют собой одну строку (или столбец, неважно) дисплея. И вот вместо 150 кБ нам требуется всего 1920 байт (320 пикселей на строку * 3 буфера * 2 байта на точку), что отлично помещается в память. Второй хак основан на том, что расчет матриц преобразования и координат вершин нельзя проводить для каждой строки, иначе изображение будет искажаться самыми причудливыми способами, да и по скорости это невыгодно. Вместо этого «внешние» расчеты, то есть перемножение матриц трансформации и их применение к вершинам пересчитываются на каждом кадре, после чего преобразуются в промежуточное представление, которое оптимизировано для рендеринга в картинку 320х1.

Из хулиганских соображений снаружи библиотека будет напоминать OpenGL. Как и в оригинальной OpenGL отрисовка начинается с формирования матрицы преобразования — очистка glLoadIdentity() делает текущую матрицу единичной, потом набор преобразований glRotateXY(...), glTranslate(…), каждое из которых умножается на текущую матрицу. Поскольку эти расчеты будут проводиться только один раз за кадр, особых требований к скорости нет, можно обойтись простыми float, без извращений с числами с фиксированной точкой. Сама матрица представляет собой массив float[4][4], отображенный на одномерный массив float[16] — вообще-то этот способ обычно применяется для динамических массивов, но и из статических можно извлечь немного выгоды. Еще один стандартный хак: вместо постоянного вычисления синусов и косинусов, которых в матрицах поворота немало, посчитаем их заранее и запишем в табличку. Для этого поделим полный круг на 256 частей, вычислим значение синуса для каждой и свалим его в массив sin_table[]. Ну а получить из синуса косинус сможет любой, кто учился в школе. Стоит отметить, что функции поворота принимают угол не в радианах, а в долях от полного оборота, после приведения к диапазону [0… 255]. Впрочем, реализованы и «честные» функции, выполняющие преобразование из угла в доли под капотом.

Когда матрица готова, можно приступить к отрисовке примитивов. Вообще, в трехмерной графике есть три типа примитивов — точка, линия и треугольник. Но если нас интересуют полигональные модели, внимание следует уделить только треугольнику. Его «отрисовка» происходит в функции glDrawTriangle() или glDrawTriangleV(). Слово «отрисовка» поставлено в кавычки потому что никакой отрисовки на данном этапе не происходит. Мы всего лишь умножаем все точки примитива на матрицу трансформации, после чего выковыриваем из них аналитические формулы ребер y = ky*x + by, которые позволяют найти пересечения всех трех ребер треугольника с текущей выводимой строкой. Одну из них отбросим, поскольку она лежит не на отрезке между вершин, а на его продолжении. То есть для отрисовки кадра нужно всего лишь пройти по всем строкам и для каждой закрасить область между точками пересечения. Но если применить этот алгоритм «в лоб», каждый примитив будет перекрывать те, что были нарисованы раньше. Нам же нужно учитывать Z-координату (глубину), чтобы треугольники пересекались красиво. Вместо простого вывода точки за точкой будем считать ее Z-координату и по сравнению с Z-координатой, хранимой в буфере глубины, либо выводить (обновляя Z-буфер), либо игнорировать. А для расчета Z-координаты каждой точки интересующей нас строки воспользуемся той же формулой прямой линии z = kz*y + bz, вычисленной по тем самым двум точкам пересечения с ребрами. В результате объект «полуфабрикатного» треугольника struct glTriangle состоит из трех X-координат вершин (хранить Y и Z-координаты смысла нет, они будут вычисляться) и k, b коэффициентов прямых, ну и цвет до кучи. Вот здесь, в отличие от расчета матриц преобразования, скорость критична, поэтому уже используем числа с фиксированной точкой. Причем если для слагаемого b достаточно той же точности, что для координат (2 байта), то точность множителя k чем больше, тем лучше, поэтому берем 4 байта. Но не float, поскольку работа с целыми числами все равно быстрее, даже при одинаковом размере.

Итак, вызвав кучу glDrawTriangle() мы подготовили массив полуфабрикатных треугольников. В моей реализации треугольники выводятся по одному явными вызовами функции. На самом деле было бы логично завести массив треугольников с адресами вершин, но здесь я решил не усложнять. Все равно функция отрисовки пишется роботами, а им без разницы, заполнять ли константный массив или писать триста одинаковых вызовов. Пришло время перевести полуфабрикаты треугольников в красивую картинку на экране. Для этого вызывается функция glSwapBuffers(). Как и было описано выше, она проходит по строкам дисплея, ищет для каждой точки пересечения со всеми треугольниками и рисует отрезки в соответствии с фильтрацией по глубине. После рендеринга каждой строки нужно эту строку отправить на дисплей. Для этого запускается DMA, которому указывается адрес строки и ее размер. А пока DMA работает, можно переключиться на другой буфер и рендерить уже следующую строку. Главное не забыть дождаться окончания передачи если вдруг закончили отрисовку раньше. Для визуализации соотношения скоростей я добавил включение красного светодиода после окончания рендеринга и выключение после завершения ожидания DMA. Получается что-то вроде ШИМ, который регулирует яркость в зависимости от времени ожидания. Теоретически вместо «тупого» ожидания можно было бы использовать прерывания DMA, но тогда я не умел ими пользоваться, да и алгоритм существенно бы усложнился. Для демонстрационной программы это излишне.

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

Что ж, если ядро простаивает, надо его нагрузить. А нагружать его будем более качественными моделями. Правда, не стоит забывать, что память все-таки сильно ограничена, так что слишком много полигонов контроллер не потянет физически. Простейший расчет показал, что после вычитания памяти на буфер строки и тому подобное, осталось место на 378 треугольников. Как показала практика, под этот размер отлично подходят модели из старой, но интересной игры Gothic. Собственно, оттуда были выдернуты модельки шныга и кровавой мухи (а уже при написании этой статьи и мракориса, красующегося на КДПВ), после чего у контроллера кончилась флеш-память. Но игровые модельки не предназначены для использования микроконтроллером.

Скажем, они содержат анимацию, текстуры и тому подобное, что нам не пригодится, да и не поместится в память. К счастью, blender позволяет не только пересохранить их в *.obj, более поддающийся парсингу, но и уменьшить количество полигонов если нужно. Дальше при помощи простенькой самописной программы obj2arr *.obj файлы разбираются на координаты, из которых впоследствии формируется *.h файл для непосредственного включения в прошивку.

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

Расчет освещения в его примитивном варианте заключается в расчете скалярного произведения нормали и направления на источник света с последующим умножением на «родной» цвет грани.
Моделек у нас теперь три — две из игры и одна тестовая, с которой начинали. Для их переключения воспользуемся одной из двух кнопок, распаянных на плате. Заодно можно добавить контроль загруженности процессора. Один контроль у нас уже есть — красный светодиод, связанный с временем ожидания DMA. А вторым, зеленым, светодиодом, будем мигать при каждом обновлении кадра — так мы сможем оценить частоту кадров. Для невооруженного глаза она составила что-то около 15 fps.


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

Если вдруг кому-то будет интересно, исходный код доступен тут.




Комментарии (46):

  1. mmMike
    /#21469868 / +4

    как впихнуть невпихуемое и отобразить в реальном времени трехмерную графику при помощи контроллера, у которого недостаточно ни скорости, ни памяти для этого.

    Это громко сказано… про недостаточность памяти и скорости. Если сравнить со "спектрум" Z80.
    Игрушки были приличные.

    • COKPOWEHEU
      /#21470608

      Не воспринимайте излишне серьезно. Будь у контроллера объективно недостаточно ресурсов, это и сделать было бы невозможно.
      А на счет Спектрума — мне казалось, у него все-таки была какая-то аппаратная видеокарта. Да и вообще специфика разная.

      • mmMike
        /#21470822

        У Спектрума аппаратная часть на рассыпухе "отображала" часть ОЗУ в видеосигналы (если упрощенно).
        С учетом того, что ili9341 это то же память + аппаратная часть, а запись через SPI + DMA в ОЗУ ili9341, в общем то, сравнима с прямой записью в ОЗУ на типичных 8Мгц Z80…
        Те же яйца, только вид сбоку.


        И я не принял серьезно :) Вот если бы на AVR 8 битовом… что гораздо ближе к Спектруму по быстродействию и памяти.

        • DolphinSoft
          /#21474814

          >Вот если бы на AVR 8 битовом… что гораздо ближе к Спектруму по быстродействию и памяти.

          Так-то AVR8, без зазрения совести:

          ЭМУЛИРУЕТ Спектрум!

          • da-nie
            /#21474902

            Там, помнится, было два AVR. Один — эмуляция Z80, а второй эмуляция периферии.

            • DolphinSoft
              /#21475334

              Видео на втором, все остальное на первом.
              И для звука еще один мелкий вроде.

      • da-nie
        /#21470856 / +1

        Нет, у спектрума нет вообще ничего для аппаратного ускорения. Но весь экран 6144 байта (чёрно-белый)+768 байт атрибуты цветов.

  2. LibrarianOok
    /#21470038 / +1

    Не было ли желания замахнуться на реализацию реймаршера на таком аппарате?

    • COKPOWEHEU
      /#21470654

      Нет. Эта поделка — разовая, она не для какой-то цели или отработки алгоритма для дальнейшего применения. Просто было интересно что можно выжать, и то без фанатизма. Изучать другие низкоуровневые алгоритмы вывода изображений на примере stm-ки смысла нет: мощность невелика, а все оптимизации будут жестко завязаны на ее же специфику.

      • LibrarianOok
        /#21470726

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

        • Zolg
          /#21473320

          и возведения в половинную степень вместо извлечения корня

          Это разные записи одной и той же операции

          • LibrarianOok
            /#21473848

            Так-то да. Только скорость выполнения разная.

            • Zolg
              /#21474408

              А есть бенчмарки?
              Я сильно сомневаюсь, что более универсальная функция pow() оптимизирована лучше специализированной sqrt(). Тем более, что последняя на многих платформах реализована аппаратно.
              Если уж говорить про "грязную скорость" sqrt() (вернее 1/sqrt(), что более востребовано), то вспоминать нужно про 0x5f3759df

              • LibrarianOok
                /#21475308

                Эта квейковская загогулина работает не только лишь везде, а вообще мало где. Она расчитана под чистый, незамутнённый 32-битный float, и ни на что больше. Бенчмарки надо разумеется писать. Вполне может оказаться, что для ваших случаев функция отработает быстрее арифметического оператора возведения в степень "^", который я использовал вместо math.pow(). В моих поделиях оператор отработал даже быстрее переопределённой функции local pow = math.pow
                Заодно вопхну сюда ответ на вопрос про число PI, поскольку набежавшие в срачик про мед. маски «специалисты по всем вопросам» подслили карму и писать комменты получается теперь не чаще раза в час.

                PI=4
                image

                • Zolg
                  /#21475882

                  Эта квейковская загогулина работает не только лишь везде, а вообще мало где. Она расчитана под чистый, незамутнённый 32-битный float, и ни на что больше

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


                  Бенчмарки надо разумеется писать

                  раз вы утверждаете, что a быстрее b, то либо вы бенчмарки писали/проводили, либо ваше утверждение голословно


                  оператора возведения в степень "^", который я использовал вместо math.pow()

                  я не знаю, про какой именно язык программирования вы говорите, но в любом случае бинарный оператор это просто сахар для вызова функции/метода.

                  • LibrarianOok
                    /#21476458

                    я не знаю, про какой именно язык программирования вы говорите, но в любом случае бинарный оператор это просто сахар для вызова функции/метода.

                    Я там опечатался в ответе. Не math.pow() имелось в виду, а math.sqrt(), по сравнению с оператором "^". Ну, немного подредактировав бенчмарки, можно сравнить например «math.pow(a)» c «a*a» и с «a^2». Касательно квадратного корня получилось вот так:
                    local a = os.clock()
                    local b = 0.0
                    for i = 1, 100000000 do
                    	b = math.sqrt(35.73)
                    end
                    a = os.clock() - a
                    print (a)

                    lua ./sqrt_lua.lua
                    14.738681
                    luajit ./sqrt_lua.lua
                    0.073986
                    

                    local sqrt = math.sqrt
                    local a = os.clock()
                    local b = 0.0
                    for i = 1, 100000000 do
                    	b = sqrt(35.73)
                    end
                    a = os.clock() - a
                    print (a)
                    

                    lua ./local_sqrt_lua.lua
                    9.867505
                    luajit ./local_sqrt_lua.lua
                    0.073703
                    

                    local a = os.clock()
                    local b = 0.0
                    for i = 1, 100000000 do
                    	b = 35.73^0.5
                    end
                    a = os.clock() - a
                    print (a)

                    lua ./test_operator.lua
                    2.276411
                    luajit ./test_operator.lua
                    0.073202
                    

                    • Zolg
                      /#21476770 / +1

                      Это прекрасно во всем: и использование lua для теста числодробильни и надежда на то, что 35.73^0.5 вычисляется более одного раза при интерпретации (и хотябы один раз в скомпилированном коде).


                      замените код вашего бенчмарка хотябы на


                      local a = os.clock()
                      local b = 100.0
                      for i = 1, 100000000 do
                          b = math.sqrt(b)*10.0
                      end
                      a = os.clock() - a
                      print (a)

                      local a = os.clock()
                      local b = 100.0
                      for i = 1, 100000000 do
                          b = (b^0.5)*10.0
                      end
                      a = os.clock() - a
                      print (a)

                      и поделитесь результатом )

                      • LibrarianOok
                        /#21479414

                        Забавно. В luajit все три варианта показали примерно одинаковую скорость. А вот в lua, как таковом, внезапно быстрее оказалась локально объявленная функция.

                        • Zolg
                          /#21479676

                          luajit все три варианта показали примерно одинаковую скорость

                          Вы не догадываетесь, почему?
                          Скорее всего ситуацию поменяет банальный print(b) в самом конце программы (даже после print(a)). Если же компилятор уапще умный, то добавьте ввод b, как параметра командной строки

        • Tsvetik
          /#21473918

          А что будет с картинкой, если пи принять равным 4?

  3. kanvas
    /#21470472

    Подскажите, возможно есть русскоязычные ссылки как на STM32 реализовать DMA с участием внешней микросхемы статической или динамической ОЗУ? Задача стоит связать STM32+ SRAM 32Kx8 + АЦП 20мегавыборок/сек с параллельной шиной данных + вывод результатов из SRAM 32Кх8 в режиме DMA на ЖК экран (128х160 или 240х320 и подобный из диапазона 150-200/шт). Как реализуемо впринципе такое?

    • mmMike
      /#21470868

      На русском… сомневаюсь.


      Смотрите в сторону FSMC функционала. Как раз для работы с SRAM и для вывода на экран в через параллельную шину.
      Я такое делал.
      https://github.com/mmMikeKn/CNC/tree/master/src/liblcd

    • COKPOWEHEU
      /#21471168

      А надо ли тут DMA? Если не ошибаюсь, у некоторых контроллеров есть аппаратный интерфейс специально для микросхема памяти, FSMC.
      То, что вы описали про АЦП, память и дисплей похоже на цифровой осциллограф, но тут хорошо бы начать с приблизительной оценки — а потянет ли контроллер столько? Что он с этими данными делать будет? Если просто отобразить на дисплее, так там 32 кБ не нужно, хватит тех же 320 байт. Ну пусть 640 если по 2 байта на отсчет. Если передавать в компьютер, то по какому интерфейсу? USB тоже не потянет.
      Да даже если АЦП будет выдавать по 20 мегасемплов в секунду, сколько тактов у контроллера уйдет чтобы их забрать и хотя бы сложить в память, не говоря уж об обработке?

  4. Akon32
    /#21470578

    Эпично.


    Я правильно понимаю, что у вас сцена рисуется построчно, по 240 раз на кадр?


    А вы не хотите заморочиться и далее, релиазовав поддержку чтения формата Binary GLTF? Этот формат более дружелюбен к современным графическим api, чем obj.

    • COKPOWEHEU
      /#21471262

      Не совсем. То есть сцена действительно рисуется построчно и строк 240, но на каждую строку рисуется только та строка примитива, которая в нее попадает. В отличие от «нормальной» графики, где примитив рисуется сразу целиком, потом другой примитив, тоже целиком. Тут в чем-то даже похоже на трассировку лучей. Хотя скорее даже «трассировка веера» если так можно сказать.
      На счет glTF, то до вашего вопроса даже не знал что это такое :) Как бы то ни было, развивать именно трехмерку на stm32 я не планирую. Но не исключено, что воспользуюсь в более «взрослой» графике. Спасибо что упомянули про такой формат.

    • lain8dono
      /#21473840

      Этот формат дружелюбен к современным графическим api

      Он дружелюбен (прибит гвоздями) к OpenGL, а не к современным графическим API. JSON-часть этого формата не выглядит хорошим решением для микроконтроллеров. Если выкинуть JSON, то остаётся просто блоб, который загружать даже проще, чем Wavefront OBJ. Достаточно memcpy или аналога, идентичного натуральному.


      OpenGL к современным графическим API я не отношу в силу того, что архитектура OpenGL идёт в разрез с тем, как устроенны современные GPU.

  5. andreykorol
    /#21470616

    Спасибо. Если добавить акселерометр-гироскоп-магнитомер, то возможно было бы получить прикольное управление моделью — как если ее через некое окошко рассматриваешь с разных сторон.

    • COKPOWEHEU
      /#21471270

      Ну в чем-то да :)
      Правда, чтобы рассмотреть модель снизу придется встать в очень интересную позу

      • scg
        /#21472552

        Достаточно лечь на спину. Для меня — это самая естественная поза :)

  6. yarston
    /#21471500

    Раз уж есть расчёт освещения, можно отбрасывать все полигоны, у которых вектор нормали направлен от экрана — их всё равно не видно. Примерно вполовину количество полигонов для отрисовки уменьшится.

    • COKPOWEHEU
      /#21471544

      Файл src/gl.c, строка 282 именно это и делает. Но это вообще стандартный метод удаления невидимых граней при обходе по часовой стрелке или против, тут вообще нет специфики применения контроллера.

  7. axe_chita
    /#21472058

    Спасибо за статью, прочиталось запоем до последней точки. Ну и для себя надо взять на заметку такую искусную работу с младшей STM32, как пример «Вижу цель, не вижу препятствия!» ;)

  8. EddyEm
    /#21472614 / -3

    Залез в код, ожидал сначала увидеть ужасный калокуб… Но увидел отличные исходники!
    Спасибо!!! Хоть немного людей в этом мире еще не превратились в хомячков…

    • haqreu
      /#21473714

      Люблю воинственные заявления, спасибо. Только в вашем же журнале вы регулярно жалуетесь на то, что бьётесь-бьётесь, а в итоге железо было неправильно инициализировано…

  9. DolphinSoft
    /#21472678

    Спасибо за статью!
    Хороший и показательный пример использования ресурсов.

    От себя добавлю:

    По сравнению с AVR характеристики камня довольно приличные: 72 МГц тактовой (на практике можно разогнать до 100 МГц, а то и больше, но только на свой страх и риск!), 20 кБ оперативки и 64 кБ флеша.

    Разгоняется до 144 МГц (из периферии становится недоступным только USB), в работе на такой частоте — чуть теплый.
    А флеша у этого чипа 128кб. :) (Для демки — самое то).

    • COKPOWEHEU
      /#21472828

      144 МГц и 128 кБ это все же как повезет. То есть производитель не только из жадности промаркировал камень меньшим номиналом, что-то на предельных режимах работает недостаточно устойчиво. А значит, в серьезной разработке полагаться на эти возможности нельзя. Для себя же, когда проверил конкретный камень — почему бы и нет, на свой страх и риск.

      • DolphinSoft
        /#21472936

        Частота официальна, не заявлена ввиду отсутствия делителя для USB.
        И само-собой, это не «в продакшн».
        Хотя все протестированные экземпляры пилюль, показали полную работоспособность флеша выше 64кб.
        Но я не настаиваю, на нет и суда нет ;)
        Просто для такой задачи, это «самое оно».

  10. dmitry_shashkov
    /#21473734 / +3

    Забавно, я пишу сейчас как раз собственный 3д движок под stm32f103 на том же железе. Чтобы прыгнуть выше 15 фпс надо отказаться от z-буффера в пользу сортировки полигонов. Ну и да, я извратился с числами с фиксированной точкой, чтобы избежать флоатов. Модельки пока не грузил. Пока как-то так, на кубах, есть куда оптимизировать. Пилю ещё серию видео на эту тему для ютубчика. ФПС где-то под 30 на глаз, бодро работает.

    image

    • COKPOWEHEU
      /#21473776

      Если вы обратили внимание, в моей демке при выводе пересекающихся квадратов довольно ярко горит красный светодиод, то есть узкое место не в математике, а в пересылке по SPI. Вот на модельках из 200 треугольников (если вычесть отброшенные по отрицательной нормали) там да, проблема в обсчете. Но и то только из-за того что я не заморачивался с оптимизацией. Точки пересечения со строкой считаются в лоб, через умножения, глубина тоже. По-хорошему, стоило бы применить алгоритм Брезенхема и проводить расчет итеративно, за одно сложение и одно сравнение.
      Z-координату вообще можно вытянуть из уравнения плоскости:
      Z = (D — x*nx — y*ny)/nz,
      D = px*nx + py*ny + pz*nz,
      где nx,ny,nz — нормаль
      pz,py,pz — опорная точка (скажем, любая вершина), но она пересчитывается в одну скалярную константу D
      x,y — координаты на плоскости, z — глубина
      — Но в целом необходимости в этом немного: в моем варианте память контроллера кончилась ненамного позже запаса быстродействия, выигрыш будет хорошо если в 2 раза на сложных модельках

      • DolphinSoft
        /#21474224

        SPI у него тоже разгоняется до 195/2 MHz, и работает без нареканий. (Неофициально, само-собой)

  11. loltrol
    /#21474422

    Ловите фаната gothic — использованы модельки шныга и кровавой мухи :)

    • COKPOWEHEU
      /#21474602

      Там еще на превьюшке третья моделька
      А вообще, ловите второго фаната Готики — он узнал эти модельки :)