Почему куб отрисовывается неправильно?

Пытаюсь отрисовать куб на С++, однако выглядит это не совсем так, как хотелось бы. Я использую формулы для преобразования сферических координат в декартовы (прямоугольные) координаты. Идея заключается в том, чтобы найти куда попадёт луч из каждого пикселя окна. Если луч встретил объект отрисовать этот пиксель.

Вот основные части кода:

int Screen[320 * 180 * 3] = { 0 };
int World[160][90][90] = { 0 };
int X1 = 0;
int Y1 = 0;
int BasicAngle = 0;

    World[10][10][0] = 1;
    World[10][10][10] = 1;
    World[20][20][20] = 1;
    World[40][40][40] = 1;
    World[40][20][10] = 1;

    createWindow(0, 0, 320, 180);

        for (int y = 0; y < 90; y++)
        {
            double B = (90.0 / 90.0) * y;
            for (int x = 0; x < 160; x++)
            {
                double A = (90.0 / 160.0) * x + BasicAngle;
                for (int R = 0; R < 100; R++)
                {
                    int X = X1 + R * sin(B * M_PI / 180) * cos(A * M_PI / 180);
                    int Y = Y1 + R * sin(B * M_PI / 180) * sin(A * M_PI / 180);
                    int Z = R * cos(B * M_PI / 180);

                    if (X < 160 && Y < 90 && X > -1 && Y > -1 && Z > -1 && Z < 90)
                    {
                        if (World[X][Y][Z] == 1)
                        {
                            R = 100;

                            Screen[y * 320 * 3 + (x + 160) * 3 + 0] = 255; // red правый
                            Screen[y * 320 * 3 + (x + 160) * 3 + 1] = 0; // green правый
                            Screen[y * 320 * 3 + (x + 160) * 3 + 2] = 0; // blue правый



                            Screen[Y * 320 * 3 + X * 3 + 0] = 255; // red левый
                            Screen[Y * 320 * 3 + X * 3 + 1] = 0; // green левый
                            Screen[Y * 320 * 3 + X * 3 + 2] = 0; // blue левый
                        }
                        else
                        {
                            Screen[Y * 320 * 3 + X * 3 + 0] = 0; // red левый
                            Screen[Y * 320 * 3 + X * 3 + 1] = 255; // green левый
                            Screen[Y * 320 * 3 + X * 3 + 2] = 0; // blue левый
                        }
                    }
                }
            }
        }

Из-за чего происходит такое искажение? Если есть другой способ выпускать луч, я могу рассмотреть эту возможность. Может проблема в определении пересечения луча и объекта? Я хочу хранить объекты в виде вокселей, после уже из кубиков собрать нечто интересное, без полигонов.

Дополнение.

Скриншоты будут делаться при нажатии на пробел.

#include <Windows.h>
#include <iostream>
#include <fstream>
#include <cmath>
double M_PI = 3.1415926535;

void save1dArrayToBMP(const char* path, int* buffer, int width, int height)
{
    std::ofstream file;
    file.open(path, std::ios::out | std::ios::binary);

    unsigned char bmpPadding[3] = { 0, 0, 0 };
    const int paddingAmount = (4 - (width * 3) % 4) % 4;

    const int fileHeaderSize = 14;
    const int informationHeaderSize = 40;
    const int fileSize = fileHeaderSize + informationHeaderSize + width * height * 4;

    unsigned char fileHeader[fileHeaderSize];

    //File type
    fileHeader[0] = 'B';
    fileHeader[1] = 'M';
    // File size
    fileHeader[2] = fileSize;
    fileHeader[3] = fileSize >> 8;
    fileHeader[4] = fileSize >> 16;
    fileHeader[5] = fileSize >> 24;
    //Reserved 1 (not used)
    fileHeader[6] = 0;
    fileHeader[7] = 0;
    //Reserved 2 (not used)
    fileHeader[8] = 0;
    fileHeader[9] = 0;
    //Pixel data offset
    fileHeader[10] = fileHeaderSize + informationHeaderSize;
    fileHeader[11] = 0;
    fileHeader[12] = 0;
    fileHeader[13] = 0;

    unsigned char informationHeader[informationHeaderSize];

    //Header size
    informationHeader[0] = informationHeaderSize;
    informationHeader[1] = 0;
    informationHeader[2] = 0;
    informationHeader[3] = 0;
    //Image width
    informationHeader[4] = width;
    informationHeader[5] = width >> 8;
    informationHeader[6] = width >> 16;
    informationHeader[7] = width >> 24;
    //Image height
    informationHeader[8] = height;
    informationHeader[9] = height >> 8;
    informationHeader[10] = height >> 16;
    informationHeader[11] = height >> 24;
    //Planes
    informationHeader[12] = 1;
    informationHeader[13] = 0;
    //Bits per pixels (RGB)
    informationHeader[14] = 24;
    informationHeader[15] = 0;
    //Compression (no compression)
    informationHeader[16] = 0;
    informationHeader[17] = 0;
    informationHeader[18] = 0;
    informationHeader[19] = 0;
    //Image size (no compression)
    informationHeader[20] = 0;
    informationHeader[21] = 0;
    informationHeader[22] = 0;
    informationHeader[23] = 0;
    //X pixels per meter (not specified)
    informationHeader[24] = 0;
    informationHeader[25] = 0;
    informationHeader[26] = 0;
    informationHeader[27] = 0;
    //Y pixels per meter (not specified)
    informationHeader[28] = 0;
    informationHeader[29] = 0;
    informationHeader[30] = 0;
    informationHeader[31] = 0;
    //Total colors (colors palette not used)
    informationHeader[32] = 0;
    informationHeader[33] = 0;
    informationHeader[34] = 0;
    informationHeader[35] = 0;
    //Important colors (Generally ignored)
    informationHeader[36] = 0;
    informationHeader[37] = 0;
    informationHeader[38] = 0;
    informationHeader[39] = 0;

    file.write(reinterpret_cast<char*>(fileHeader), fileHeaderSize);
    file.write(reinterpret_cast<char*>(informationHeader), informationHeaderSize);
    //file.write(reinterpret_cast<char*>(buffer), width * height * 4);

    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            unsigned char r = static_cast<unsigned char>(buffer[(height - y - 1) * width * 3 + x * 3 + 0]);
            unsigned char g = static_cast<unsigned char>(buffer[(height - y - 1) * width * 3 + x * 3 + 1]);
            unsigned char b = static_cast<unsigned char>(buffer[(height - y - 1) * width * 3 + x * 3 + 2]);

            unsigned char color[] = { b, g, r };


            file.write(reinterpret_cast<char*>(color), 3);
        }
        file.write(reinterpret_cast<char*>(bmpPadding), paddingAmount);
    }

    file.close();

    std::cout << "File created\n";
}

int World[160][90][90] = { 0 };
int X1 = 0;
int Y1 = 0;
int BasicAngle = 0;
int Screen[320 * 180 * 3] = { 0 };

void drawLine(int x1, int y1, int z1, int x2, int y2, int z2, int ScreenX, int ScreenY) {
    int dx = x2 - x1;
    int dy = y2 - y1;
    int dz = z2 - z1;

    int steps = max(max(abs(dx), abs(dy)), abs(dz));  // Выбираем максимальное изменение по координатам как количество шагов

    int xInc = dx / steps;
    int yInc = dy / steps;
    int zInc = dz / steps;

    int x = x1;
    int y = y1;
    int z = z1;

    for (int i = 0; i <= steps; i++) {

        if (x < 160 && y < 90 && x > -1 && y > -1 && z > -1 && z < 90)
        {
            if (World[x][y][z] == 1)
            {
                i = steps;

                Screen[ScreenY * 320 * 3 + (ScreenX + 160) * 3 + 0] = 255; // red
                Screen[ScreenY * 320 * 3 + (ScreenX + 160) * 3 + 1] = 0; // green
                Screen[ScreenY * 320 * 3 + (ScreenX + 160) * 3 + 2] = 0; // blue

                Screen[y * 320 * 3 + x * 3 + 0] = 255; // red
                Screen[y * 320 * 3 + x * 3 + 1] = 0; // green
                Screen[y * 320 * 3 + x * 3 + 2] = 0; // blue
            }
            else
            {
                Screen[y * 320 * 3 + x * 3 + 0] = 0; // red
                Screen[y * 320 * 3 + x * 3 + 1] = 255; // green
                Screen[y * 320 * 3 + x * 3 + 2] = 0; // blue
            }
        }
        x += xInc;
        y += yInc;
        z += zInc;
    }
}

int main()
{
    World[10][10][0] = 1;
    World[10][10][10] = 1;
    World[20][20][20] = 1;
    World[40][40][40] = 1;
    World[40][20][10] = 1;
    while (true)
    {
        for (int i = 0; i < 320 * 180 * 3; i++)
        {
            Screen[i] = 0;
        }
        for (int x = 0; x < 320; x++)
        {
            Screen[90 * 320 * 3 + x * 3 + 0] = 255; // red
            Screen[90 * 320 * 3 + x * 3 + 1] = 255; // green
            Screen[90 * 320 * 3 + x * 3 + 2] = 255; // blue
        }
        for (int y = 0; y < 180; y++)
        {
            Screen[y * 320 * 3 + 160 * 3 + 0] = 255; // red
            Screen[y * 320 * 3 + 160 * 3 + 1] = 255; // green
            Screen[y * 320 * 3 + 160 * 3 + 2] = 255; // blue
        }
        for (int y = 0; y < 90; y++)
        {
            double B = (90.0 / 90.0) * y;
            for (int x = 0; x < 160; x++)
            {
                double A = (90.0 / 160.0) * x + BasicAngle;

                int R = 100;
                int X = X1 + R * sin(B * M_PI / 180) * cos(A * M_PI / 180);
                int Y = Y1 + R * sin(B * M_PI / 180) * sin(A * M_PI / 180);
                int Z = R * cos(B * M_PI / 180);

                drawLine(X1, Y1, 0, X, Y, Z, x, y);
            }
        }
        if (GetAsyncKeyState(VK_SPACE))
        {
            save1dArrayToBMP("Frame.bmp", Screen, 320, 180);
            Sleep(100);
        }
    }
}

23.12.2023: Получилось добиться некоторого результата с подобием треугольников. Пока что попробовал только в 2Д. Выглядит это следующим образом:

Добавил алгоритм Брезенхема:

Площадь с алгоритмом только уменьшилась. Угол обзора влияет на кучность лучей в центре, однако я не могу уменьшать угол бесконечно (увеличивая фокусное расстояние, катет). Что можно с этим сделать? (Код без алгортима)

int Focus = 32;
for (int x = -15; x < 16; x++)
{
    double L = sqrt(Focus * Focus + x * x);
    for (double Lenght = 0; Lenght < 100; Lenght += 0.1)
    {
        double coeff = Lenght / L;
        int X = x * coeff;
        int Y = Focus * coeff;

        Screen[Y * 320 * 3 + (X + 160) * 3 + 1] = 255;
    }
}

Тут в коде от -15 до 16, из-за этого пустоты, а надо от -159 до 160. С этим разобрался и перешёл в 3D

Перевёл в 3D 1 строкой:

double L = sqrt(Focus * Focus + x * x + y * y);

Очень хороший результат, что мне не нравится - волнистость кубика, неровные грани.

Алгоритм Брезенхема в 3D не могу добавить, не понимаю почему. А вот шаг повысил чёткость, однако сильно упала производительность. Теперь шаг 0.01, а хотелось бы 0.5

Я не могу понять следующее:

  1. Почему не работает алгоритм Брезенхема?
  2. Как умножать координаты на матрицу (чтобы повернуть камеру), если мне нужно в результате получить опять координаты, а не матрицу?
  3. Как можно обойти искажение без шага? Очень сильно упала производительность.

30.12.23 Последние результаты такие, для разрешения 640х480 дальность прорисовки от 130 до примерно 150 единиц, слева направо:

  1. 8.323 с. (метод на подобии треугольников, шаг: 0.01)
  2. 9.648 с. (метод по уравнению прямой, шаг на умножении: 0.00001)
  3. 6.824 с. (метод по уравнению прямой, шаг на сложении: 0.00001)
  4. 0.479 с. (код Станислава, принцип я пока что не понял)

Нужно что-то такое:

во втором варианте я использую следующий способ:

в циклах for меняю endX и endY:

int aX = endX - startX;
int aY = endY - startY;
int aZ = endZ - startZ;

и после в цикле for меняю множитель (шаг):

int aX1 = aX * multiplier;
int aY1 = aY * multiplier;
int aZ1 = aZ * multiplier;

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

При этом сетка проходит по 0, по 1, по 2 и т.д., а куб находится между 0 и 1. В идеале сетка должна проходить по 0.5, по 1.5, 2.5 и т.д. - это уже задел на будущее

31.12: Попробовал я демонстрационный пример. Попробовал пример - не изучил, очень постараюсь с ним разобраться, т.к. хотелось бы менять размер сетки, для оптимизации по типу "Voxel Octrees". С примером работает в 20 раз быстрее, чем просто с шагом. При разрешении 640х480 всё хорошо, потому что соотношение сторон почти одинаковое, а вот при FHD и соотношении 16:9 уже начинаются проблемы:

Жёлтым это призма (угол обзора), красным сам куб. Пытался домножать aX (чуть выше) на какой-то коэффициент, однако не преуспел. Меняется пропорция по высоте или ширине (цифры, где я указал количество пикселей), однако левый-нижний угол и диагональ правого-верхнего угла продолжают различаться, что плохо. Пока что я не понимаю, что и на что требуется умножить?


Ответы (2 шт):

Автор решения: Stanislav Volodarskiy

введите сюда описание изображения

#include <stdbool.h>
#include <stdio.h>

typedef struct {
    bool busy;
    unsigned char r, g, b;
} voxel_t;

#define GRID_SIZE 10
voxel_t grid[GRID_SIZE][GRID_SIZE][GRID_SIZE];

int main() {

    grid[0][0][1].busy = true;
    grid[0][0][1].r = 0;
    grid[0][0][1].g = 0;
    grid[0][0][1].b = 0;

    grid[0][0][5].busy = true;
    grid[0][0][5].r = 0;
    grid[0][0][5].g = 0;
    grid[0][0][5].b = 127;

    grid[0][0][9].busy = true;
    grid[0][0][9].r = 0;
    grid[0][0][9].g = 0;
    grid[0][0][9].b = 255;

    grid[9][0][1].busy = true;
    grid[9][0][1].r = 255;
    grid[9][0][1].g = 0;
    grid[9][0][1].b = 0;

    grid[9][0][5].busy = true;
    grid[9][0][5].r = 255;
    grid[9][0][5].g = 0;
    grid[9][0][5].b = 127;

    grid[9][0][9].busy = true;
    grid[9][0][9].r = 255;
    grid[9][0][9].g = 0;
    grid[9][0][9].b = 255;

    grid[0][9][1].busy = true;
    grid[0][9][1].r = 0;
    grid[0][9][1].g = 255;
    grid[0][9][1].b = 0;

    grid[0][9][5].busy = true;
    grid[0][9][5].r = 0;
    grid[0][9][5].g = 255;
    grid[0][9][5].b = 127;

    grid[0][9][9].busy = true;
    grid[0][9][9].r = 0;
    grid[0][9][9].g = 255;
    grid[0][9][9].b = 255;

    grid[9][9][1].busy = true;
    grid[9][9][1].r = 255;
    grid[9][9][1].g = 255;
    grid[9][9][1].b = 0;

    grid[9][9][5].busy = true;
    grid[9][9][5].r = 255;
    grid[9][9][5].g = 255;
    grid[9][9][5].b = 127;

    grid[9][9][9].busy = true;
    grid[9][9][9].r = 255;
    grid[9][9][9].g = 255;
    grid[9][9][9].b = 255;

    const int dimx = 640, dimy = 480;

    // camera origin position
    double origin_x = 5;
    double origin_y = 5;
    double origin_z = -15;

    // screen plane dimensions
    double screen_z_offset = 0.5;
    double screen_dimx = 0.4;
    double screen_dimy = 0.3;

    FILE *fp = fopen("temp.ppm", "wb");
    fprintf(fp, "P6\n%d %d\n255\n", dimx, dimy);

    // (i, j) - pixel on screen (pixel in image)
    for (int i = 0; i < dimy; ++i) {
        for (int j = 0; j < dimx; ++j) {

            // pixel_* - координаты пиксела в мировой системе координат

            // [0, dimx - 1] -> [-0.5, 0.5]
            double sx = (double)j / (dimx - 1) - 0.5;
            // [-0.5, 0.5] ->
            // [origin_x - screen_dimx / 2, origin_x + screen_dimx / 2] 
            double pixel_x = origin_x + sx * screen_dimx;

            // [0, dimy - 1] -> [0.5, -0.5]
            double sy = 0.5 - (double)i / (dimy - 1);
            // [-0.5, 0.5] ->
            // [origin_y - screen_dimy / 2, origin_y + screen_dimy / 2] 
            double pixel_y = origin_y + sy * screen_dimy;

            double pixel_z = origin_z + screen_z_offset;

            // направление луча из камеры в мир
            double dx = pixel_x - origin_x;
            double dy = pixel_y - origin_y;
            double dz = pixel_z - origin_z;
            printf("%lf %lf %lf\n", dx, dy, dz);

            // текущая точка на луче
            double x = origin_x;
            double y = origin_y;
            double z = origin_z;

            // индексы текущего вокселя
            int vx = (int)x;
            int vy = (int)y;
            int vz = (int)z;

            unsigned char color[3] = {127, 127, 127};

            // трассировка вдоль луча
            while (vz < GRID_SIZE) {

                double dtz = (vz + 1 - z) / dz;

                double dtx = dtz + 1;
                if (dx > 0) {
                    dtx = (vx + 1 - x) / dx;
                } else if (dx < 0) {
                    dtx = (vx - x) / dx;
                }

                double dty = dtz + 1;
                if (dy > 0) {
                    dty = (vy + 1 - y) / dy;
                } else if (dy < 0) {
                    dty = (vy - y) / dy;
                }

                double dt = dtx;
                if (dty < dt) {
                    dt = dty;
                }
                if (dtz < dt) {
                    dt = dtz;
                }
                if (dt == dtx) {
                    if (dx > 0) {
                        ++vx;
                    } else if (dx < 0) {
                        --vx;
                    }
                }
                if (dt == dty) {
                    if (dy > 0) {
                        ++vy;
                    } else if (dy < 0) {
                        --vy;
                    }
                }
                if (dt == dtz) {
                    ++vz;
                }
                x += dt * dx;
                y += dt * dy;
                z += dt * dz;

                if (
                    0 <= vx && vx < GRID_SIZE &&
                    0 <= vy && vy < GRID_SIZE &&
                    0 <= vz && vz < GRID_SIZE &&
                    grid[vx][vy][vz].busy
                ) {
                    color[0] = grid[vx][vy][vz].r;
                    color[1] = grid[vx][vy][vz].g;
                    color[2] = grid[vx][vy][vz].b;
                    break;
                }
            }

            fwrite(color, 1, 3, fp);
        }
    }

    fclose(fp);
}
→ Ссылка
Автор решения: Stanislav Volodarskiy

Демонстрационный пример, который показывает как по отрезку вывести индексы всех кубов, которые отрезок пересекает:

#include <math.h>
#include <stdio.h>

typedef struct {
    int    i;       // текущий индекс ячейки решётки
    double dt;      // шаг между пересечениями
    double next_t;  // момент следующего пересечения решётки
    int    di;      // как меняется индекс при пересечении решётки
} range_t;

void init_range(double start, double end, range_t *r) {
    r->i = (int)floor(start);
    r->dt = 1. / fabs(end - start);
    if (start < end) {
        r->next_t = r->dt * (floor(start) + 1. - start);
        r->di = 1;
    } else if (start > end) {
        r->next_t = r->dt * (start - floor(start));
        r->di = -1;
    } else {
        r->next_t = 2;  // заведомо большое значение
        r->di = 0;
    }
}

int main() {

    double sx = 3.5;
    double sy = 9.9;
    double sz = 8.9;

    double ex = 9.9;
    double ey = 0.1;
    double ez = 5.2;

    range_t rx;
    range_t ry;
    range_t rz;

    init_range(sx, ex, &rx);
    init_range(sy, ey, &ry);
    init_range(sz, ez, &rz);

    double t = 0;
    while (t < 1) {
        // печать координат куба
        printf("%i %i %i\n", rx.i, ry.i, rz.i);

        // rx->next_t - параметр t, когда отрезок пересечёт целый x
        // ry->next_t - параметр t, когда отрезок пересечёт целый y
        // rz->next_t - параметр t, когда отрезок пересечёт целый z
        // выбираем из них наименьший
        range_t *nearest = &rx;
        if (ry.next_t < nearest->next_t) {
            nearest = &ry;
        }
        if (rz.next_t < nearest->next_t) {
            nearest = &rz;
        }

        t = nearest->next_t;  // увеличиваем параметр t

        nearest->next_t += nearest->dt;  // запоминаем следующее пересечение
        nearest->i += nearest->di;       // исправляем координату куба
    }
}
$ gcc rasterization.c -lm

$ ./a.out 
3 9 8
4 9 8
4 8 8
4 7 8
5 7 8
5 7 7
5 6 7
6 6 7
6 5 7
6 4 7
6 4 6
7 4 6
7 3 6
8 3 6
8 2 6
8 2 5
8 1 5
9 1 5
9 0 5
→ Ссылка