Как известно, старшие STM'ки имеют приличные частоты и объёмы ОЗУ. Ну а раз так, то почему бы не запустить 3D-графику на таких контроллерах? Да нет ничего проще!

Демонстрационная картинка

Для отображения 3D графики я подключил к плате STM32F4Discovery на базе STM32F407 дисплей с разрешением 320x240. Нет, не по FSMC — у этой платы заняты нужные контакты. Впрочем, для наших экспериментов хватит и обычных портов.
Дисплей подключается вот так:
  • CS -> E12
  • RST -> E2
  • RS -> E15
  • WR -> E14
  • RD -> E13
  • D0 -> E4
  • D1 -> E5
  • D2 -> E6
  • D3 -> E7
  • D4 -> E8
  • D5 -> E9
  • D6 -> E10
  • D7 -> E11


В репозитории есть классы для трёх разных вариантов дисплея (IL9325, SPFD5408, HX8347D).


Внешний вид

Для рисования 3D-графики я когда-то очень давно делал некое подобие фрагмента библиотеки OpenGL первой версии. Вот это подобие я и приделал к STM32. На один пиксель эта библиотека требует 6 байт (2 на цвет и 4 на Z-буфер). Памяти у STM32F407 в этом варианте хватит только на 160x120. Ну что ж, значит, растянем картинку в два раза по вертикали и по горизонтали.
Данное поделие умеет:
1) Текстурирование текстурой с размерами, кратными степени двойки;
2) Z-отсечение;
3) Интерполяцию цвета вершин внутри грани;
4) Расчёт освещения для восьми источников с настройкой затухания от расстояния.

Порядок работы с библиотекой следующий:
Сперва нужно инициализировать библиотеку (она выделит память под экран). Затем, как и у OpenGL, нужно настроить матрицу проецирования и видовой порт.
 const int32_t WIDTH=160;
 const int32_t HEIGHT=120;
 const float VISIBLE_ANGLE=60;
 const float NEAR_PLANE=1;
 const float FAR_PLANE=1000;	
 float aspect=static_cast<float>(WIDTH)/static_cast<float>(HEIGHT);
 		
 cSGL.Init(WIDTH,HEIGHT);
 cSGL.Perspective(VISIBLE_ANGLE,aspect,NEAR_PLANE,FAR_PLANE);
 cSGL.SetViewport(0,0,WIDTH,HEIGHT);


Собственно, теперь можно и рисовать, практически так же, как и в OpenGL (для аналогичных команд).

Например, можно задать источник света:
 cSGL.MatrixMode(CSGL::SGL_MATRIX_MODELVIEW);
 cSGL.LoadIdentity();
 float l0_position[]={0,0,0};
 float l0_ambient[]={0.1,0.1,0.1};
 float l0_diffuse[]={0.7,0.7,0.7};
 float l0_specular[]={1,1,1};
 float l0_shininess[]={1};
 cSGL.Lightfv(CSGL::SGL_LIGHT0,CSGL::SGL_POSITION,l0_position);
 cSGL.Lightfv(CSGL::SGL_LIGHT0,CSGL::SGL_AMBIENT,l0_ambient);
 cSGL.Lightfv(CSGL::SGL_LIGHT0,CSGL::SGL_DIFFUSE,l0_diffuse);
 cSGL.Lightfv(CSGL::SGL_LIGHT0,CSGL::SGL_SPECULAR,l0_specular);
 cSGL.Lightfv(CSGL::SGL_LIGHT0,CSGL::SGL_SHININESS,l0_shininess);


Можно задать материал для поверхности:
 float m0_ambient[]={0.1,0.1,0.1};
 float m0_diffuse[]={0.5,0.5,0.5};
 float m0_specular[]={0.5,0.5,0.5};
 float m0_emission[]={0.1,0.1,0.1};
 cSGL.Materialfv(CSGL::SGL_AMBIENT,m0_ambient);
 cSGL.Materialfv(CSGL::SGL_DIFFUSE,m0_diffuse);
 cSGL.Materialfv(CSGL::SGL_SPECULAR,m0_specular); 
 cSGL.Materialfv(CSGL::SGL_EMISSION,m0_emission); 


Можно включить расчёт освещения cSGL.Enable(CSGL::SGL_LIGHTING); и конкретный источник света cSGL.Enable(CSGL::SGL_LIGHT0);
Освещённость одинаково считается для передней и задней стороны. Более того, отсечения задних граней я не делал (оно редко бывает мне нужно). Но при желании сделать его пара пустяков.

Создавать источники света не обязательно. Вполне можно задать цвет граней самим, используя cSGL.Color3f. Цвет (и параметры цвета материала тоже) задаётся нормированный [0..1];

Текстура подключается командой cSGL.BindTexture. Обращаю внимание, что библиотека использует текстурирование всегда! При этом массив цветов текстуры задаётся как последовательность четырёх байт (r,g,b,alpha) и вот тут r,g,b,alpha числа от 0 до 255. Строго говоря, именно на эти числа и умножается интенсивность каналов R,G,B, задаваемая glColor3f (или рассчитанная по данным источников света) при отрисовке. Массив данных текстуры должен сохраняться на всё время вывода грани.

Вывод грани осуществляется следующим образом:
  
  cSGL.Begin();
   cSGL.TexCoordf(0,0);	
   cSGL.Vertex3f(x6,y6,z6);
   cSGL.TexCoordf(0,1);	 
   cSGL.Vertex3f(x4,y4,z4);
   cSGL.TexCoordf(1,0);	 
   cSGL.Vertex3f(x2,y2,z2);
  cSGL.End();


cSGL.Begin включает рисование грани, cSGL.End завершает этот режим. Внутри этих команд помещаются команды задания параметров точки (нормаль, нормированная текстурная координата, нормированный цвет точки) и отрисовки точки cSGL.Vertex3f. Количество точек внутри begin/end может быть произвольным и в целом составляет выводимый выпуклый многоугольник.

Управление текстурированием, проецированием и моделированием осуществляется, как и в OpenGL тремя матрицами, выбираемыми командами
MatrixMode(SGL_MATRIX_TEXTURE);
MatrixMode(SGL_MATRIX_PROJECTION);
MatrixMode(SGL_MATRIX_MODELVIEW);

Операции с этими матрицами выполняются командами
LoadIdentity();
cSGL.Rotatef(angle,0,0,1);
cSGL.Translatef(-0.5,-0.5,0);


Для рисования библиотека использует класс точки CGLScreenColor. Это влияет на быстродействие не в лучшую сторону, но позволяет легко переносить библиотеку между разными дисплеями и архитектурами.
Какой сейчас получился FPS? Для данного октаэдра без одной грани (я её снял) замеры длительности, собственно, процедур отрисовок дают около 150-200 FPS. А вот переброска через порты буфера на дисплей с удвоением картинки существенно снижает FPS, примерно до 10-15 (да-да, я даже не думал это оптимизировать пока что).
Обращаю внимание, что библиотека не оптимизировалась практически ни в одном месте. Её задача была просто сделать понятный инструмент 3D-рисования с возможностью модернизации. Полагаю, оптимизировав можно ускорить вывод графики на STM32 ещё раз в 5-10 (например, применив FSMC, оптимизировав рисование и переброску данных дисплею).

Для чего может пригодиться такая библиотека? Ну, например, выводить какие-либо показания пользователю в наглядном виде. Например, ориентацию простенького 3D объекта.

Видео работы:


Репозиторий для STM32
Репозиторий для Windows.

Удачи в развитии проекта! :)