ESP, IoT 3D рендерер

e3mca9

✩✩✩✩✩✩✩
13 Янв 2024
1
0
Добрый день.
Пытаюсь написать рабочий 3д рендерер на есп8266. Использую олед дисплей на i2c, к нему библиотеку GyverOLED. Как основу взял код с этого репозитория и интерпретировал насколько позволяют руки под arduino ide.

Собственно столкнулся с весьма затруднительной проблемой:
Рисую стандартный куб из полигонов через проекционную матрицу и все ок, красивый кубик появляется на экране.
Screenshot_607.png
Но стоит добавить кубу вращение через матрицу, то изображение сходит с ума.
Screenshot_608.png

Код:
3D_Render_Test:
#include <GyverOLED.h>
#include <Wire.h>
#include <vector>
using namespace std;

GyverOLED<SSD1306_128x64, OLED_BUFFER> oled;

// Переменная простого 3д вектора
struct vec3d
{
  float x, y, z;
};

// Треугольник из трех 3д векторов
struct triangle
{
  vec3d p[3];
};

// Абстрактный 3д объект состоящий из треугольников
// !!!Поскольку я планировал использовать не только куб, вместо массива я использую vector.
// !!!из параллельного проекта я чудом понял что esp дружит и с векторами, и с std библиотекой, уже проверял
struct mesh
{
  vector<triangle> tris;
};

// Матрица 4х4
struct mat4x4
{
  float m[4][4];
};

// Объявление куба и матрицы проекции из 3д в 2д
mesh meshCube;
mat4x4 matProj;

// Тэта используется как переменная вращения. Все таймы для просчета deltaTime, обозначения кадров в секунду и тд.
float fTheta;
float fOldTime, fCurrentTime, fDeltaTime;


// Переменные касающиеся фрастума(границ рендера) и проекции
// Near и Far -- ближняя и дальняя граница отрисовки
// Fov -- угол обзора или же фокусное расстояние
// AspectRatio говорит само за себя. Соотношение сторон экрана. Значения берутся -1 так как отрисовка экрана начинается с 0
// FovRad размер координат экрана
float fNear = 0.1f;
float fFar = 20.0f;
float fFov = 90.0f;
float fAspectRatio = 63.0f / 127.0f;
float fFovRad = 1.0f / (float)tan(fFov * 0.5f / 180.0f * 3.14159f);

void setup()
{
  Serial.begin(115200);
  //Wire.begin();
  //Wire.setClock(400000UL);

  // Попытка рендерить не полный куб, а лишь одну его сторону. Убрать комментарий и закоментировать переменную ниже, если надо попробовать
  /*
  meshCube.tris = {
    { -1.0f, -1.0f, -1.0f,    -1.0f, 1.0f, -1.0f,    1.0f, 1.0f, -1.0f },
    { -1.0f, -1.0f, -1.0f,    1.0f, 1.0f, -1.0f,    1.0f, -1.0f, -1.0f }
  };
  */

  // Обозначение всех треугольников в кубе и их вершин
  ///*
  meshCube.tris = {

    // SOUTH
    { -1.0f, -1.0f, -1.0f,    -1.0f, 1.0f, -1.0f,    1.0f, 1.0f, -1.0f },
    { -1.0f, -1.0f, -1.0f,    1.0f, 1.0f, -1.0f,    1.0f, -1.0f, -1.0f },

    // EAST                                                     
    { 1.0f, -1.0f, -1.0f,    1.0f, 1.0f, -1.0f,    1.0f, 1.0f, 1.0f },
    { 1.0f, -1.0f, -1.0f,    1.0f, 1.0f, 1.0f,    1.0f, -1.0f, 1.0f },

    // NORTH                                                     
    { 1.0f, -1.0f, 1.0f,    1.0f, 1.0f, 1.0f,    -1.0f, 1.0f, 1.0f },
    { 1.0f, -1.0f, 1.0f,    -1.0f, 1.0f, 1.0f,    -1.0f, -1.0f, 1.0f },

    // WEST                                                     
    { -1.0f, -1.0f, 1.0f,    -1.0f, 1.0f, 1.0f,    -1.0f, 1.0f, -1.0f },
    { -1.0f, -1.0f, 1.0f,    -1.0f, 1.0f, -1.0f,    -1.0f, -1.0f, -1.0f },

    // TOP                                                       
    { -1.0f, 1.0f, -1.0f,    -1.0f, 1.0f, 1.0f,    1.0f, 1.0f, 1.0f },
    { -1.0f, 1.0f, -1.0f,    1.0f, 1.0f, 1.0f,    1.0f, 1.0f, -1.0f },

    // BOTTOM                                                   
    { 1.0f, -1.0f, 1.0f,    -1.0f, -1.0f, 1.0f,    -1.0f, -1.0f, -1.0f },
    { 1.0f, -1.0f, 1.0f,    -1.0f, -1.0f, -1.0f,    1.0f, -1.0f, -1.0f },

    };
    //*/

    // Обозначение переменных в матрице проекции. Есть разные варианты из разных источников, по факту меняется лишь
    // направленность осей x-y
    matProj.m[0][0] = fAspectRatio * fFovRad;
    matProj.m[1][1] = fFovRad;
    matProj.m[2][2] = fFar / (fFar - fNear);
    matProj.m[3][2] = (-fFar * fNear) / (fFar - fNear);
    matProj.m[2][3] = 1.0f;
    matProj.m[3][3] = 0.0f;

    oled.init();

    //delay(200);
}

void loop()
{
  oled.clear();

  //Просчет deltaTime
  deltaTime();

  // Основная функция просчета отрисовки объекта
  drawTriangles();

  // Вывод времени между кадрами на дисплей
  oled.setCursor(0, 0);
  oled.setScale(1);
  oled.print(fDeltaTime);
 
  oled.update();

  //delay(100);
}

void deltaTime()
{
  fOldTime = fCurrentTime;
  fCurrentTime = millis();
  fDeltaTime = fCurrentTime - fOldTime;
}

void drawTriangles()
{
  // Переменные матриц вращения по Z и X координате
  mat4x4 matRotZ, matRotX;

  // Тэта, влияющая на степень вращения объекта. Сейчас стоит 1 для отладки. Для вращения поменять на fTheta + fDeltaTime * 0.1 или еще меньше
  fTheta = 1;

  // Матрица вращения по оси Z
  matRotZ.m[0][0] = cos(fTheta);
  matRotZ.m[0][1] = sin(fTheta);
  matRotZ.m[1][0] = -sin(fTheta);
  matRotZ.m[1][1] = cos(fTheta);
  matRotZ.m[2][2] = 1.0f;
  matRotZ.m[3][3] = 1.0f;

  //Serial.println(cos(fTheta));

  // Матрица вращения по оси X
  matRotX.m[0][0] = 1.0f;
  matRotX.m[1][1] = (float)cosf(fTheta * 0.5f);
  matRotX.m[1][2] = (float)sinf(fTheta * 0.5f);
  matRotX.m[2][1] = (float)-sinf(fTheta * 0.5f);
  matRotX.m[2][2] = (float)cosf(fTheta * 0.5f);
  matRotX.m[3][3] = 1.0f;

  // Основной цикл всех преобразований матрицы и отрисовки
  for(int i = 0; i < sizeof(meshCube.tris); i++)
  {
    // Локальные буфферы для каждого треугольника
    triangle tri, triProjected, triTranslated, triRotatedZ, triRotatedZX;
    tri = meshCube.tris[i];
    //Serial.println(String(triRotatedZ.p[0].x) + " , " + String(triRotatedZ.p[0].y) + " , " + String(triRotatedZ.p[0].z));
    ///*

    // Преобразование координат объекта через матрицу вращения по Z
    // Rotate in Z-Axis
    MultiplyMatrixVector(tri.p[0], triRotatedZ.p[0], matRotZ);
    MultiplyMatrixVector(tri.p[1], triRotatedZ.p[1], matRotZ);
    MultiplyMatrixVector(tri.p[2], triRotatedZ.p[2], matRotZ);

    // Аналогично по X, уже после преобразования по Z
    /*
    // Rotate in X-Axis
    MultiplyMatrixVector(triRotatedZ.p[0], triRotatedZX.p[0], matRotX);
    MultiplyMatrixVector(triRotatedZ.p[1], triRotatedZX.p[1], matRotX);
    MultiplyMatrixVector(triRotatedZ.p[2], triRotatedZX.p[2], matRotX);
    */
    
    // Смещение объекта подальше от "камеры", вглубь сцены. Менять послений + float. При значениях выше 5-6 могут начаться
    // артефакты ввиду слишком низкого разрешения экрана
    triTranslated = triRotatedZ;
    triTranslated.p[0].z = triRotatedZ.p[0].z + 3.0f;
    triTranslated.p[1].z = triRotatedZ.p[1].z + 3.0f;
    triTranslated.p[2].z = triRotatedZ.p[2].z + 3.0f;

    // Для проверки как рисуется куб без вращения закоментить четыре строки выше и разкоментить четыре ниже.
    /*
    triTranslated = triRotatedZ;
    triTranslated.p[0].z = triRotatedZ.p[0].z + 3.0f;
    triTranslated.p[1].z = triRotatedZ.p[1].z + 3.0f;
    triTranslated.p[2].z = triRotatedZ.p[2].z + 3.0f;
    */

    //Serial.println(triRotatedZ.p[0].x);
    //*/

    // Проекция 3д объекта со всеми вращениями на 2д плоскость
    MultiplyMatrixVector(triTranslated.p[0], triProjected.p[0], matProj);
    MultiplyMatrixVector(triTranslated.p[1], triProjected.p[1], matProj);
    MultiplyMatrixVector(triTranslated.p[2], triProjected.p[2], matProj);

    // Так как объект рендерится в пространстве от -1 до 1, а дисплей показывает только от 0 и выше, то надо объект
    // сдвинуть на положительную ось координат
    // Далее плоскость делится в пополам относительно размера дисплея
    triProjected.p[0].x += 1.0f; triProjected.p[0].y += 1.0f;
    triProjected.p[1].x += 1.0f; triProjected.p[1].y += 1.0f;
    triProjected.p[2].x += 1.0f; triProjected.p[2].y += 1.0f;
    triProjected.p[0].x *= 0.5f * 127.0f;
    triProjected.p[0].y *= 0.5f * 63.0f;
    triProjected.p[1].x *= 0.5f * 127.0f;
    triProjected.p[1].y *= 0.5f * 63.0f;
    triProjected.p[2].x *= 0.5f * 127.0f;
    triProjected.p[2].y *= 0.5f * 63.0f;

    // Вызов функции отрисовки целого треугольника. По факту лишь вызов трех функций .line из библиотеки GyverOLED
    drawTriangle(triProjected.p[0].x, triProjected.p[0].y,
        triProjected.p[1].x, triProjected.p[1].y,
        triProjected.p[2].x, triProjected.p[2].y,
        1);
    //delay(500);
  }
}

// страшная функция преобразования координат и выбранной матрицы
// я взял "как есть" из репозитория, но тут особо иначе и не сделаешь, смотрел и в других проектах, которые сложновато было найти
// &i входной вектор
// &о выходной вектор, в него обратно и записывается все
// &m выбор матрицы для преобразования
void MultiplyMatrixVector(vec3d &i, vec3d &o, mat4x4 &m)
{
  o.x = i.x * m.m[0][0] + i.y * m.m[1][0] + i.z * m.m[2][0] + m.m[3][0];
  o.y = i.x * m.m[0][1] + i.y * m.m[1][1] + i.z * m.m[2][1] + m.m[3][1];
  o.z = i.x * m.m[0][2] + i.y * m.m[1][2] + i.z * m.m[2][2] + m.m[3][2];
  float w = i.x * m.m[0][3] + i.y * m.m[1][3] + i.z * m.m[2][3] + m.m[3][3];

  if (w != 0.0f)
  {
    o.x /= w;
    o.y /= w;
    o.z /= w;
  }
}

void drawTriangle(int x1, int y1, int x2, int y2, int x3, int y3, uint8_t color)
{
  oled.line(x1, y1, x2, y2, color);
  oled.line(x2, y2, x3, y3, color);
  oled.line(x3, y3, x1, y1, color);
}

Из того, что я смог сам увидеть через дебаг:
1. Просчет положения точек в пространстве через sin() и cos() либо зацикливается на паре одинаковых чисел, либо вовсе выдает nan, соответственно ничего не рисуя на экран.
Screenshot_605.png
2. При небольшом времени работы плата начинает перезагружаться с ошибками в сериал порте.
Screenshot_606.png
3. При попытке изменить объект с куба до простой плоскости из двух полигонов плата начинает выдавать какие-то заоблачные цифры в х-координате, сама себя перезапускает и в итоге чуть ли не сгорает. В первый раз так чудом удалось ее сбросить и перепрошить.

Что пытался сделать и не помогло:
-Перевести как можно больше переменных вращения из double в float для экономии памяти
-Аналогично но обратно. Все матрицы вращения принудительно оставить в double для "точности вычисления"
-Разные порядки строк и колонн в матрицах, ибо не исключено что язык или компилятор могут отличаться от С++
-Мониторить отдельно переменные fTheta, fDeltaTime, выходные значения на синусах и косинусах матрицы вращения, координаты точек как в 3д пространстве, так и преобразованное в 2д
-Попытаться применить чуть более простую формулу вращения через псевдо-проекцию, но результат был схож
-Замедлить скорость просчета переменных и отрисовки

Касательно моих мыслей на счет проблемы.
1. Я думаю функции sin() и cos() уж больно сильно жрут способности платы. На аналогичном проекте с дисплеем я собрал алгоритм пиксельной сортировки, и все работало хорошо до момента когда я решил прикрутить один маленький просчет "заполненности" цвета через синусоиду. Значения как-то менялись, а вот дисплей намертво встал в отрисовке. Точно уже не вспомню и не скажу что именно менялось, входная переменная в sin() или же переменная выход с него, но факт что после его вызова просто ничего не работало. В конце концов именно на преобразованиях с синусами и косинусами все начинает ломаться.
2. Либо это аномальное поведение функции MultiplyMatrixVector. Как ее можно изменить, не сломав все и запутавшись еще больше, я не нашел. И почему конкретно перемножение синусов и косинусов так влияет на результат
Как правильно работать с arduino ide и конкретно с платами esp я не знаю. Какие-то знания в программировании у меня есть, но где прочитать все особенности микроконтроллера, какие у него "горлышки бутылки" есть и прочие приколы я не нашел. Если говорить про работоспособность кода, то аналогичную программу я делал на компьютере в С++ и выводом изображение в окошко, вроде бы все работало.

Если кто-то сможет понять, то буду очень рад прочитать развернутое объяснение кто и в каком месте тут не прав, хотя и не думаю что тут много людей интересующихся графикой на микроконтроллерах

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