Добрый день.
Пытаюсь написать рабочий 3д рендерер на есп8266. Использую олед дисплей на i2c, к нему библиотеку GyverOLED. Как основу взял код с этого репозитория и интерпретировал насколько позволяют руки под arduino ide.
Собственно столкнулся с весьма затруднительной проблемой:
Рисую стандартный куб из полигонов через проекционную матрицу и все ок, красивый кубик появляется на экране.
Но стоит добавить кубу вращение через матрицу, то изображение сходит с ума.

Код:
Из того, что я смог сам увидеть через дебаг:
1. Просчет положения точек в пространстве через sin() и cos() либо зацикливается на паре одинаковых чисел, либо вовсе выдает nan, соответственно ничего не рисуя на экран.

2. При небольшом времени работы плата начинает перезагружаться с ошибками в сериал порте.

3. При попытке изменить объект с куба до простой плоскости из двух полигонов плата начинает выдавать какие-то заоблачные цифры в х-координате, сама себя перезапускает и в итоге чуть ли не сгорает. В первый раз так чудом удалось ее сбросить и перепрошить.
Что пытался сделать и не помогло:
-Перевести как можно больше переменных вращения из double в float для экономии памяти
-Аналогично но обратно. Все матрицы вращения принудительно оставить в double для "точности вычисления"
-Разные порядки строк и колонн в матрицах, ибо не исключено что язык или компилятор могут отличаться от С++
-Мониторить отдельно переменные fTheta, fDeltaTime, выходные значения на синусах и косинусах матрицы вращения, координаты точек как в 3д пространстве, так и преобразованное в 2д
-Попытаться применить чуть более простую формулу вращения через псевдо-проекцию, но результат был схож
-Замедлить скорость просчета переменных и отрисовки
Касательно моих мыслей на счет проблемы.
1. Я думаю функции sin() и cos() уж больно сильно жрут способности платы. На аналогичном проекте с дисплеем я собрал алгоритм пиксельной сортировки, и все работало хорошо до момента когда я решил прикрутить один маленький просчет "заполненности" цвета через синусоиду. Значения как-то менялись, а вот дисплей намертво встал в отрисовке. Точно уже не вспомню и не скажу что именно менялось, входная переменная в sin() или же переменная выход с него, но факт что после его вызова просто ничего не работало. В конце концов именно на преобразованиях с синусами и косинусами все начинает ломаться.
2. Либо это аномальное поведение функции MultiplyMatrixVector. Как ее можно изменить, не сломав все и запутавшись еще больше, я не нашел. И почему конкретно перемножение синусов и косинусов так влияет на результат
Как правильно работать с arduino ide и конкретно с платами esp я не знаю. Какие-то знания в программировании у меня есть, но где прочитать все особенности микроконтроллера, какие у него "горлышки бутылки" есть и прочие приколы я не нашел. Если говорить про работоспособность кода, то аналогичную программу я делал на компьютере в С++ и выводом изображение в окошко, вроде бы все работало.
Если кто-то сможет понять, то буду очень рад прочитать развернутое объяснение кто и в каком месте тут не прав, хотя и не думаю что тут много людей интересующихся графикой на микроконтроллерах
P. S. Уверен конкретно на arduino ide есть куда более оптимизированный код и использование методов о которых я даже не слышал, но интересно было вывести простенький рендерер чтобы потом добавлять функций и собрать интересный проект
Пытаюсь написать рабочий 3д рендерер на есп8266. Использую олед дисплей на i2c, к нему библиотеку GyverOLED. Как основу взял код с этого репозитория и интерпретировал насколько позволяют руки под arduino ide.
Собственно столкнулся с весьма затруднительной проблемой:
Рисую стандартный куб из полигонов через проекционную матрицу и все ок, красивый кубик появляется на экране.

Но стоит добавить кубу вращение через матрицу, то изображение сходит с ума.

Код:
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, соответственно ничего не рисуя на экран.

2. При небольшом времени работы плата начинает перезагружаться с ошибками в сериал порте.

3. При попытке изменить объект с куба до простой плоскости из двух полигонов плата начинает выдавать какие-то заоблачные цифры в х-координате, сама себя перезапускает и в итоге чуть ли не сгорает. В первый раз так чудом удалось ее сбросить и перепрошить.
Что пытался сделать и не помогло:
-Перевести как можно больше переменных вращения из double в float для экономии памяти
-Аналогично но обратно. Все матрицы вращения принудительно оставить в double для "точности вычисления"
-Разные порядки строк и колонн в матрицах, ибо не исключено что язык или компилятор могут отличаться от С++
-Мониторить отдельно переменные fTheta, fDeltaTime, выходные значения на синусах и косинусах матрицы вращения, координаты точек как в 3д пространстве, так и преобразованное в 2д
-Попытаться применить чуть более простую формулу вращения через псевдо-проекцию, но результат был схож
-Замедлить скорость просчета переменных и отрисовки
Касательно моих мыслей на счет проблемы.
1. Я думаю функции sin() и cos() уж больно сильно жрут способности платы. На аналогичном проекте с дисплеем я собрал алгоритм пиксельной сортировки, и все работало хорошо до момента когда я решил прикрутить один маленький просчет "заполненности" цвета через синусоиду. Значения как-то менялись, а вот дисплей намертво встал в отрисовке. Точно уже не вспомню и не скажу что именно менялось, входная переменная в sin() или же переменная выход с него, но факт что после его вызова просто ничего не работало. В конце концов именно на преобразованиях с синусами и косинусами все начинает ломаться.
2. Либо это аномальное поведение функции MultiplyMatrixVector. Как ее можно изменить, не сломав все и запутавшись еще больше, я не нашел. И почему конкретно перемножение синусов и косинусов так влияет на результат
Как правильно работать с arduino ide и конкретно с платами esp я не знаю. Какие-то знания в программировании у меня есть, но где прочитать все особенности микроконтроллера, какие у него "горлышки бутылки" есть и прочие приколы я не нашел. Если говорить про работоспособность кода, то аналогичную программу я делал на компьютере в С++ и выводом изображение в окошко, вроде бы все работало.
Если кто-то сможет понять, то буду очень рад прочитать развернутое объяснение кто и в каком месте тут не прав, хотя и не думаю что тут много людей интересующихся графикой на микроконтроллерах
P. S. Уверен конкретно на arduino ide есть куда более оптимизированный код и использование методов о которых я даже не слышал, но интересно было вывести простенький рендерер чтобы потом добавлять функций и собрать интересный проект