Трёхмерная модель бриллианта

Трёхмерная модель бриллианта с  изменением параметров проекции

Задача: спроектировать трёхмерную модель бриллианта и предоставить пользователю параметры настройки проекции;
Использованый API: GTK/GDK;
Среда разработки: Code::Blocks 8.02 (Linux)
Компилятор: GCC;

Данная программа считвает из файла координаты трёхмерной фигуры и проецирует её на экран с возможностью изменения проекции.
Файл должен включать в себя:
1. Количество точек и граней
2. Координаты каждой точки
3. Количество точек и номера точек, упорядоченые в одном направлении тройки векторов с остальными гранями

Для построения бриллианта я использовал пропорции

и школьные знания геометрии, такие как теорема косинусов a^2 = b^2 + c^2 − 2bc*cosα и уравнение прямой

Если внимательно посмотреть на бриллиант
то можно увидеть что он состоит из 5 слоёв точек (5 уровней по высоте):
1. Восьмиугольник (Корона)
2. Восмиугольник побольше
3. Шестнадцатиугольник (Рундист) ( в идеальном случае окружность, но поскольку нам нужно конечное количество точек, обойдёмся шестнадцатиугольником)
4. Восьмиугольник (Павильон)
5. 1 точка (Шип или колета)

Далее исходя из пропорций бриллианта я принял 1% за 1 деление моей трёхмерной системы координат, и сразу же получил несколько координат точек. Далее, зная радуис описаной окружности правильного многоугольника и угол между осью координат и радиусом, проходящего через 1у из точек многоугольника по теореме косинусов можно найти проекцию на эту ось, соответственно координату. В случае со 2ым уровнем с радиусом пришлось погадать, а в 4ом уровне его находить пришлось используя уравнения прямой, т. е. я находил точки лежащие на прямой от Шипа до одной из точек 3его уровня.

Перейдём к алгоритму отображения фигуры.

Последовательность действий:
1. Считываем координаты из файла и записываем их в структуры
2. Подключаем таймер, который будет регулярно выполнять следующие действия:
а) поворот фигуры
б) проверка видимости граней
в) прорисовка граней

Разберём подробно каждый пункт.

1. У нас имеется главная структура, которая имеет указатели на массив структур точек и массив структур граней. Каждая структура грани имеет массив указателей на указатели на точки, из которых состоит эта грань.
2. Таймер вызывает главную функцию каждые 0,035 секунды, передавая ей главную структуру. Главная функция вызывает остальные функции.
а) получив вектор вращения, нормируем его (находим длину вектора d=sqrt(x*x + y*y + z*z) и делим каждую координату на эту длину). Затем получаем матрицу поворота

где:
тета - угол поворота
Ux, Uy, Uz - координаты вектора вращения
получив матрицу поворота, поочерёдно умножаем её на координаты каждой точки фигуры и получаем новые координаты точек.
б) Находим векторное произведения 2ух векторов каждой грани по формуле:
a*b = { a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2], a[0]*b[1] - a[1]*b[0] }
Получаем нормали грани. Далее находим скалярное произведение между вектором камеры и вектором нормали по формуле:
a*b = a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
Если оно больше нуля, то грань видима, иначе невидима.
в) Делаем проход по всем граням, за каждый шаг делаем проход по всем точкам грани и вычисляя каждый раз координаты проекции точек на экран c учётом перспективы по формулам:
X = CX + mas*x/1 - z/c)
Y = CY + mas*y/1 - z/c)
где х, у, z -реальные координаты точки
X, Y - координаты проекции
CX, CY - координаты центра
c - координата z вектора камеры
теперь зная экранные координаты прорисовываем каждую видимую грань на экране сплошными линиями, а невидимые пунктиром.

Панель с кнопками, флажками, метками и полями ввода делается в gtk достаточно просто и алгоритм её построения понятно изъяснён в коментариях кода:

#include <stdlib.h>
#include <stdio.h>
#include <gtk/gtk.h>
#include <math.h>
#include <string.h>
#define c -80
 
typedef struct point point;
typedef struct edge edge;
typedef struct manager manager;
 
struct point //структура с координатами точки
{
    double ax, ay, az;
    int number;
};
struct edge //структура грани, которая содержит кол-во точек, видимость и указатель на указатель структуры с координатами
{
    int count;
    gboolean vis;
    point **np;
};
struct manager //главная структура, сожержащая в себе виджет, угол поворота, указатели на точки и грани, кол-ва точек и граней
{
    GtkWidget *wd;
    float angle;
    point *pt;
    edge *ed;
    int edcount;
    int ptcount;
};
//------------------------------------------------
//глобальные переменные, изменяемые пользователем
float glmas, glx, gly, glz; //маштаб, координаты вектора, вокруг которого вращается фигура
gboolean glf, glp; //флажки заливки и перспективы
//------------------------------------------------
void callback( GtkWidget *widget, gpointer   data ) //функция обратного вызова, которая вызывается при каждом нажатии на кнопку или флажок
{
    switch ((int)data)
    {
        case '+' : glmas+=0.2; break; //увеличиваем фигуру
        case '-' : glmas-=0.2; break; //уменьшеаем фигуру
        case 'f' : if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget))) glf=TRUE; //если флажок активен то заливаем
                   else glf=FALSE;                                                          //в ином случае не заливаем
                   break;
        case 'p' : if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget))) glp=TRUE; //аналогично с перспективой
                   else glp=FALSE;
                   break;
        default : break;
    }
}
 
void osx_callback (GtkWidget *widget, GtkWidget *osi) //функция обратного вызова, которая вызывается при каждом изменении координаты Х вектора вращения
{
    const gchar *entry_text;
    entry_text = gtk_entry_get_text (GTK_ENTRY (osi)); //получаем текст с поля ввода
    float x=*entry_text - 48; //конвертируем данные в число
    if (gly==0 && glz==0 && x==0.000000) return; //предотвращаем случай с вектором (0,0,0) иначе при нормировании вектора получим деление на 0
    else  glx=x;
    g_print ("New vector of rotating:\n%f %f %f\n", glx, gly, glz);
}
void osy_callback (GtkWidget *widget, GtkWidget *osi) //аналогично
{
    const gchar *entry_text;
    entry_text = gtk_entry_get_text (GTK_ENTRY (osi));
    float y=*entry_text - 48;
    if (glx==0 && glz==0 && y==0.000000) return;
    else  gly=y;
    g_print ("New vector of rotating:\n%f %f %f\n", glx, gly, glz);
}
void osz_callback (GtkWidget *widget, GtkWidget *osi)
{
    const gchar *entry_text;
    entry_text = gtk_entry_get_text (GTK_ENTRY (osi));
    float z=*entry_text - 48;
    if (glx==0 && gly==0 && z==0.000000) return;
    else  glz=z;
    g_print ("New vector of rotating:\n%f %f %f\n", glx, gly, glz);
}
 
 
manager *InitData() //инициализирование файла с координатами фигуры
{
    manager *mr = NULL;
    FILE *f = NULL;
    if ((f=fopen("coordinates.txt", "r"))==NULL)
    {
         g_print("Ошибка открытия файла!");
         return mr;
    }
    gint i, j;
    mr=(manager *)malloc(sizeof(manager)); //выделяем память для главной структуры
    fscanf(f, "%d %d", &mr->ptcount, &mr->edcount);//считываем кол-во точек и играней
    mr->ed=(edge *)malloc(mr->edcount*sizeof(edge));//выделяем память для массива структур с гранями
    mr->pt=(point *)malloc(mr->ptcount*sizeof(point));//выделяем память для массива структур с точками
    for(i=0; i<mr->ptcount; i++) //проходимся по координатам точек и записываем их в массив структур с точками
        fscanf(f, "%lf %lf %lf", &mr->pt[i].ax, &mr->pt[i].ay, &mr->pt[i].az);
    int t;
    for(i=0; i<mr->edcount; i++) //проходимся по граням
    {
        fscanf(f, "%d", &mr->ed[i].count); //считываем кол-во точек в этой грани
        mr->ed[i].np=(point **)malloc(mr->ed[i].count*sizeof(point *)); //выделяем необходимую память для указателей на точки
        for(j=0; j<mr->ed[i].count; j++) //проходимся по номерам точек и записываем соответствующие указатели на них
        {
            fscanf(f, "%d", &t);
            mr->ed[i].np[j]=&mr->pt[t];
            mr->pt[t].number=t;
        }
    }
    fclose(f);
    return mr;
}
void turn(float x,float y,float z, manager *m) //вращение фигуры
{
    double d=sqrt((float)x*x + y*y + z*z); //вычисляем длину вектора вращения
    x=x/d; //
    y=y/d; // нормируем его
    z=z/d; //
    double MATR[4][4]; //транспонированая матрица для поворота фигуры вокруг заданого вектора <a href="http://www.devmaster.net/wiki/images/math/fe382245e2210c46cae61f32dbb27305.png
" title="http://www.devmaster.net/wiki/images/math/fe382245e2210c46cae61f32dbb27305.png
">http://www.devmaster.net/wiki/images/math/fe382245e2210c46cae61f32dbb273...</a>    MATR[0][0] = (1-cos(m->angle))*(x*x-1)+1;   MATR[0][1] = sin(m->angle)*z+(1-cos(m->angle))*x*y; MATR[0][2] = (1-cos(m->angle))*x*z-sin(m->angle)*y;
    MATR[1][0] = (1-cos(m->angle))*x*y-sin(m->angle)*z; MATR[1][1] = (1-cos(m->angle))*(y*y-1)+1;   MATR[1][2] = (1-cos(m->angle))*y*z+sin(m->angle)*x;
    MATR[2][0] = (1-cos(m->angle))*x*z+sin(m->angle)*y; MATR[2][1] = (1-cos(m->angle))*y*z-sin(m->angle)*x; MATR[2][2] = (1-cos(m->angle))*(z*z-1)+1;
    MATR[3][3] = 1.0; MATR[1][3] = 0; MATR[2][3] = 0; MATR[3][1] = 0; MATR[3][2] = 0; MATR[0][3] = 0;
    gint i;
    double vec[4];
    for(i=0; i<m->ptcount; i++) //проходимся по всем точкам фигуры
    {
        vec[0]=m->pt[i].ax;
        vec[1]=m->pt[i].ay;
        vec[2]=m->pt[i].az;
        vec[3]=1;
        float np[4];
        gint j,n;
        for (j = 0; j < 4; j++)
        {
            double Sum = 0;
            for (n = 0; n < 4; n++)
            {
                Sum += vec[n] * MATR[n][j]; //каждую точку поворачиваем с помощью матрицы поворота
            }
            np[j] = Sum;
        }
        m->pt[i].ax=np[0];
        m->pt[i].ay=np[1];
        m->pt[i].az=np[2];
    }
}
void CheckVisibility(manager *m) //проверка видимости
{
    double x1, x2, x3, y1, y2, y3, p;
    gint i;
    for(i=0; i < m->edcount; i++)
    {
        if (glp==TRUE) p = 1 -m->ed[i].np[0]->az/c; else p=1; //проверка включённой/выключеной перспективы
        x1 = m->ed[i].np[0]->ax/p;
        y1 = m->ed[i].np[0]->ay/p;
        if (glp==TRUE) p = 1 -m->ed[i].np[1]->az/c; else p=1;
        x2 = m->ed[i].np[1]->ax/p;
        y2 = m->ed[i].np[1]->ay/p;
        if (glp==TRUE) p = 1 -m->ed[i].np[2]->az/c; else p=1;
        x3 = m->ed[i].np[2]->ax/p;
        y3 = m->ed[i].np[2]->ay/p;
    	if ( ( ((x2-x1)*(y3-y1) - (y2-y1)*(x3-x1) )*c ) > 0 ) m->ed[i].vis = TRUE; //проверка знака скалярного произведения вектора камеры и вектора нормали грани (вектор нормали находится путём векторного произведения 2ух векторов грани)
    	else m->ed[i].vis = FALSE;
    }
}
 
gint draw(GtkWidget *wid, GdkEventExpose *event, gpointer data) //рисование фигуры
{
    manager *m=(manager*)data;
    gint cx=300; //координаты центра рисования
    gint cy=300;
    gint i, n;
    double p;
    double x, y;
    double dashes[] = {20.0};
    cairo_t *cr=gdk_cairo_create(m->wd->window); //инициализация cairo
 
    for(n=0; n < m->edcount; n++) //проходися по граням
    {
        if (m->ed[n].vis == FALSE) //если грань невидима
        {
            cairo_set_line_width(cr, 1); //то устанавливаем толщину линии 1
            cairo_set_dash(cr, dashes, 1, 0); //и активируем пунктир
        }
        else //иначе
        {
            cairo_set_dash(cr, dashes, 0, 0); //убираем пунктир
            cairo_set_line_width(cr, 2); //ставим толщину линий 2
        }
        if (glp==TRUE) p = 1 -m->ed[n].np[0]->az/c; else p=1; //проверка наличия перспективы
        x = (int)(cx + glmas*m->ed[n].np[0]->ax/p); //проекция точек на экран с учётом масштаба, центра и перспективы
        y = (int)(cy + glmas*m->ed[n].np[0]->ay/p);
        cairo_move_to(cr, x, y); //начинаем нанесение маски
        for (i=1; i<m->ed[n].count; i++)//проходимся по точкам грани
        {
            if (glp==TRUE) p = (1 -m->ed[n].np[i]->az/c); else p=1;
            x = (int)(cx + glmas*m->ed[n].np[i]->ax/p);
            y = (int)(cy + glmas*m->ed[n].np[i]->ay/p);
            cairo_line_to(cr, x, y); //двигаем кисть
        }
        if (glp==TRUE) p = 1 -m->ed[n].np[0]->az/c; else p=1;
        x = (int)(cx + glmas*m->ed[n].np[0]->ax/p);
        y = (int)(cy + glmas*m->ed[n].np[0]->ay/p);
        cairo_line_to(cr, x, y); //заканчиваем нанесение маски
        cairo_set_source_rgb(cr, 0, 0, 0); //устанавливаем чёрный цвет
        if (glf==TRUE) //если заливка включена
        {
            cairo_stroke_preserve(cr); //наносим контур грани сохраняя маску
            cairo_set_source_rgba(cr, 0.2, 0.4, 0.8, 0.2); //устанавливаем цвет заливки
            cairo_fill(cr); //заливаем
        }
        else cairo_stroke(cr); //иначе рисуем контур сбрасывая маску
 
    }
    cairo_destroy(cr);//деиницализация cairo
    return 1;
}
 
void check(manager *m) //функция проверки структур с точками и гранями
{
    g_print("Points: %d Edges: %d\n", m->ptcount, m->edcount);
    gint i, j;
    for(i=0; i<m->ptcount; i++)
        g_print("%f %f %f\n", m->pt[i].ax, m->pt[i].ay, m->pt[i].az);
    for(i=0; i<m->edcount; i++)
    {
        g_print("%d : ", m->ed[i].count);
        for(j=0; j<m->ed[i].count; j++)
            g_print(" %d ", m->ed[i].np[j]->number);
       g_print("\n");
    }
}
 
gint HOST(gpointer data) // главная функция, вызываемая таймером
{
    manager *m=(manager *)data; //получаем главную структуру
    turn(glx, gly, glz, m); //поворачиваем фигуру
    CheckVisibility(m); //проверяем видимость
    gtk_widget_queue_draw(m->wd); //рисуем
    return 1;
}
int main (int argc, char *argv[])
{
  GtkWidget *wid; //окно
  GtkWidget *area; //область рисования
  GtkWidget *table; //таблица
  GtkWidget *button; //кнопка
  GtkWidget *flag; //флажок
  GtkWidget *label; //надпись
  GtkWidget *osi; //поле ввода
 
 
 
 
  /* Initialize GTK+ */
  g_log_set_handler ("Gtk", G_LOG_LEVEL_WARNING, (GLogFunc) gtk_false, NULL);
  gtk_init (&argc, &argv);
  g_log_set_handler ("Gtk", G_LOG_LEVEL_WARNING, g_log_default_handler, NULL);
 
  manager *m;
  m=InitData(); //инициализируем данные
  if (m == NULL) return 1;
  m->angle=M_PI/180; //устанавливаем угол вражения 1 градус
 
  /* Create the main window */
  wid = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  gtk_window_set_title (GTK_WINDOW (wid), "3D");
  gtk_container_set_border_width (GTK_CONTAINER (wid), 0);
 
 
  gtk_window_set_position (GTK_WINDOW (wid), GTK_WIN_POS_CENTER); //располагаем окно по центру
  gtk_widget_set_size_request(wid, 800, 600); //устанавливаем размер окна
  gtk_drawing_area_size(area, 600, 600); //размер области рисования
  gtk_widget_realize (wid);
  table = gtk_table_new (24, 12, TRUE); //создаём таблицу 24х12
  gtk_container_add (GTK_CONTAINER (wid), table); //добавляем таблицу в контейнер
  area =  gtk_drawing_area_new(); //инициализируем область рисования
 
  gtk_table_attach_defaults (GTK_TABLE (table), area, 0, 9, 0, 24); //размещаем область рисования в таблице
  gtk_widget_show (area);
    //------------------
    //ПАНЕЛЬ УПРАВЛЕНИЯ
    //------------------
  button = gtk_button_new_with_label ("Увеличить"); //инициализируем кнопочку увеличения
  g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (callback), (gpointer)'+'); //подключаем сигнал нажатия на кнопку, при активации передаём символ '+'
  gtk_table_attach_defaults (GTK_TABLE (table), button, 9, 12, 0, 2); //размещаем кнопку в таблице
  gtk_widget_show (button);
 
  button = gtk_button_new_with_label ("Уменьшить");//аналогично
  g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (callback), (gpointer)'-');
  gtk_table_attach_defaults (GTK_TABLE (table), button, 9, 12, 2, 4);
  gtk_widget_show (button);
 
  flag = gtk_check_button_new_with_label ( "Заливка" );
  gtk_toggle_button_set_active( flag, TRUE );//устанавливаем флажог в активное состояние
  g_signal_connect (G_OBJECT (flag), "clicked", G_CALLBACK (callback), (gpointer)'f');
  gtk_table_attach_defaults (GTK_TABLE (table), flag, 9, 12, 5, 6);
  gtk_widget_show (flag);
 
  flag = gtk_check_button_new_with_label ( "Перспектива" );
  gtk_toggle_button_set_active( flag, TRUE );
  g_signal_connect (G_OBJECT (flag), "clicked", G_CALLBACK (callback), (gpointer)'p');
  gtk_table_attach_defaults (GTK_TABLE (table), flag, 9, 12, 6, 7);
  gtk_widget_show (flag);
 
  label=gtk_label_new("Вектор вращения (x, y, z)"); //создаём надпись
  gtk_table_attach_defaults (GTK_TABLE (table), label, 9, 12, 8, 9);
  gtk_widget_show (label);
 
  osi = gtk_entry_new(); //инициализируем поле ввода координаты X
  gtk_entry_set_max_length (GTK_ENTRY (osi), 1); //устанавливаем максимальное количество введёных символов 1
  g_signal_connect (G_OBJECT (osi), "activate", G_CALLBACK (osx_callback), (gpointer) osi); //подключаем сигнал активирования поля ввода
  gtk_entry_set_text (GTK_ENTRY (osi), "1"); //устанавливаем текст по умолчанию "1"
  gtk_table_attach_defaults (GTK_TABLE (table), osi, 9, 10, 9, 10);
  gtk_widget_show (osi);
 
  osi = gtk_entry_new(); //аналогично
  gtk_entry_set_max_length (GTK_ENTRY (osi), 1);
  g_signal_connect (G_OBJECT (osi), "activate", G_CALLBACK (osy_callback), (gpointer) osi);
  gtk_entry_set_text (GTK_ENTRY (osi), "1");
  gtk_table_attach_defaults (GTK_TABLE (table), osi, 10, 11, 9, 10);
  gtk_widget_show (osi);
 
  osi = gtk_entry_new();
  gtk_entry_set_max_length (GTK_ENTRY (osi), 1);
  g_signal_connect (G_OBJECT (osi), "activate", G_CALLBACK (osz_callback), (gpointer) osi);
  gtk_entry_set_text (GTK_ENTRY (osi), "0");
  gtk_table_attach_defaults (GTK_TABLE (table), osi, 11, 12, 9, 10);
  gtk_widget_show (osi);
 
  //----------------------------------------------------------------------------------
 
  m->wd=area;
  glmas=4;
  glf=TRUE;
  glp=TRUE;
  glx=1;
  gly=1;
  glz=0;
  check(m);
  g_timeout_add(35, HOST, (gpointer)m); //подключаем таймер, который будет 1 раз в 0,035 секунды вызывасть главную функцию HOST и передавать ей главную структуру
  g_signal_connect (G_OBJECT (area), "expose_event", G_CALLBACK (draw), (gpointer)m);
  g_signal_connect (G_OBJECT (wid), "destroy", G_CALLBACK(gtk_main_quit), NULL);
 
  /* Enter the main loop */
  gtk_widget_show_all (wid);
  gtk_main ();
  return 0;
}

Ключевые слова: 
Бриллиант, паралельная проекция
ВложениеРазмер
Source.rar5.34 кб