Переименуйте данный объект, дав ему имя AnT.
Рисунок 1. Окно разрабатываемой программы.
Справа от данного элемента разместите элемент comboBox, после чего в его свойствах установите значение параметра DropDownStyle = DropDownList. После этого выпадающие элементы перестанут быть доступными для редактирования. Затем измените элементы Items, как показано на рисунке 2.
Рисунок 2. Заполнение элементов ListBox.
Помимо этого установите элемент trackBar в окно формы. Перейдя к его свойствам, установите rientation равным Vertical. Максимальный диапазон установите равным 100.
Также не забудьте установить ссылки на используемые библиотеки Tao (рис. 3).
Рисунок 3. Подключение необходимых библиотек.
Для реализации визуализации будет использоваться таймер – после инициализации окна он будет генерировать событие, называемое тиком таймера, раз в 30 миллисекунд. Добавьте элемент таймер, переименуйте экземпляр в RenderTimer и установите время тика 30 миллисекунд (как показано на рисунке 5), а также добавьте ему событие для обработки тика.
Обязательно включите таймер (свойство enabled должно быть установлено в true). Событие, которое будет выполняться при очередном тике таймера, будет вызывать функцию отрисовки сцены.
Рисунок 4. Настройка таймера.
Инициализация окна и OpenGl происходит так же, как и в предыдущих проектах.
Нам потребуется объявить ряд переменных для дальнейшей работы программы:
/*http://esate.ru, Anvi*/
private int lastX, lastY; private float rot_1, rot_2;
private double[,] GeometricArray = new double[64,3];
private double[, ,] ResaultGeometric = new double[64, 64, 3];
private int init_mode = 0;
private int count_elements = 0;
private double Angle = 2*Math.PI / 64;
private int Iter = 64;
public Form1()
{
InitializeComponent();
AnT.InitializeContexts();
}
Как и раньше функция Form1_Load отвечает за инициализацию OpenGL. Но теперь здесь еще происходит построение массива геометрии тела, построенного вращением на основе заданного заранее массива GeometricArray. Form1_Load - это функция обработчик события Load для формы, на которой мы размещаем элементы управления.
Код этой функции максимально подробно комментирован:
/*http://esate.ru, Anvi*/
private void Form1_Load( object sender, EventArgs e)
{
// инициализация Glut
Glut.glutInit();
Glut.glutInitDisplayMode( Glut.GLUT_RGB | Glut.GLUT_DOUBLE | Glut.GLUT_DEPTH);
// очистка окна
Gl.glClearColor(255, 255, 255, 1);
// установка порта вывода в соответствии с размерами элемента anT
Gl.glViewport(0, 0, AnT.Width, AnT.Height);
// настройка проекции
Gl.glMatrixMode( Gl.GL_PROJECTION);
Gl.glLoadIdentity();
Glu.gluPerspective(45, (float)AnT.Width / (float)AnT.Height, 0.1, 200);
Gl.glMatrixMode( Gl.GL_MODELVIEW);
Gl.glLoadIdentity();
// настройка параметров OpenGL для визуализации
Gl.glEnable( Gl.GL_DEPTH_TEST);
Gl.glEnable( Gl.GL_LIGHTING);
Gl.glEnable( Gl.GL_LIGHT0);
// количество элементов последовательности геометрии, на основе которых будет строится тело вращения
count_elements = 8;
// непосредственное заполнение точек.
// после изменения данной геометрии мы сразу получим новое тело вращения.
GeometricArray[0,0] = 0;
GeometricArray[0,1] = 0;
GeometricArray[0,2] = 0;
GeometricArray[1,0] = 0.7;
GeometricArray[1,1] = 0;
GeometricArray[1,2] = 1;
GeometricArray[2, 0] = 1.3;
GeometricArray[2, 1] = 0;
GeometricArray[2, 2] = 2;
GeometricArray[3,0] = 1.0;
GeometricArray[3,1] = 0;
GeometricArray[3,2] = 3;
GeometricArray[4, 0] = 0.5;
GeometricArray[4, 1] = 0;
GeometricArray[4, 2] = 4;
GeometricArray[5, 0] = 3;
GeometricArray[5, 1] = 0;
GeometricArray[5, 2] = 6;
GeometricArray[6, 0] = 1;
GeometricArray[6, 1] = 0;
GeometricArray[6, 2] = 7;
GeometricArray[7, 0] = 0;
GeometricArray[7, 1] = 0;
GeometricArray[7, 2] = 7.2f;
// по умолчанию мы будем отрисовывать фигуру в режиме GL_POINTS
comboBox1.SelectedIndex = 0;
// построение геометрии тела вращения
// принцип сводится к двум циклам: на основе первого перебираются
// вершины в геометрической последовательности,
// а второй использует параметр Iter и производит поворот последней линии геометрии вокруг центра тела вращения
// при этом используется заранее определенный угол angle, который определяется как 2*Pi / количество медиан объекта
// за счет выполнения этого алгоритма получается набор вершин, описывающих оболочку тела вращения.
// остается только соединить эти точки в режиме рисования примитивов для получения
// визуализированного объекта
// цикл по последовательности точек кривой, на основе которой будет построено тело вращения
for ( int ax = 0; ax < count_elements; ax++)
{
// цикла по медианам объекта, заранее определенным в программе
for ( int bx = 0; bx < Iter; bx++)
{
// для всех (bx > 0) элементов алгоритма используется предыдущая построенная последовательность
// для ее поворота на установленный угол
if (bx > 0)
{
double new_x = ResaultGeometric[ax, bx - 1, 0] * Math.Cos(Angle) - ResaultGeometric[ax, bx - 1, 1] * Math.Sin(Angle);
double new_y = ResaultGeometric[ax, bx - 1, 0] * Math.Sin(Angle) + ResaultGeometric[ax, bx - 1, 1] * Math.Cos(Angle);
ResaultGeometric[ax, bx, 0] = new_x;
ResaultGeometric[ax, bx, 1] = new_y;
ResaultGeometric[ax, bx, 2] = GeometricArray[ax, 2];
}
else // для построения первой медианы мы используем начальную кривую, описывая ее нулевым значением угла поворота
{
double new_x = GeometricArray[ax, 0] *Math.Cos(0) - GeometricArray[ax, 1] * Math.Sin(0);
double new_y = GeometricArray[ax, 1] *Math.Sin(0) + GeometricArray[ax, 1] * Math.Cos(0);
ResaultGeometric[ax, bx, 0] = new_x;
ResaultGeometric[ax, bx, 1] = new_y;
ResaultGeometric[ax, bx, 2] = GeometricArray[ax, 2];
}
}
}
// активация таймера
RenderTimer.Start();
}
Итак, геометрия объекта построена, остается обработать сообщение таймера для вызова функции отрисовки, а также реализовать непосредственно функцию Draw.
В функции Draw мы рассмотрим три вида визуализации, которые будут использованы в зависимости от установленного режима в элементе comboBox.
Визуализация с помощью точек – самая простая. Визуализация с помощью линий или полигонов – уже сложнее.
Постарайтесь максимально тщательно разобрать алгоритм, чтобы понять суть его работы.
/*http://esate.ru, Anvi*/
// функция обработки сообщения таймера
private void RenderTimer_Tick( object sender, EventArgs e)
{
// вызываем функцию, отвечающей за отрисовку сцены
Draw();
}
// функция отрисовки сцены
private void Draw()
{
// два параметра, которые мы будем использовать для непрерывного вращения сцены вокруг 2 координатных осей
rot_1++;
rot_2++; // очистка буфера цвета и буфера глубины
Gl.glClear( Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT);
Gl.glClearColor(255, 255, 255, 1);
// очищение текущей матрицы
Gl.glLoadIdentity();
// установка положения камеры (наблюдателя). Как видно из кода,
// дополнительно на положение наблюдателя по оси Z влияет значение,
// установленное в ползунке, доступном для пользователя.
// таким образом, при перемещении ползунка, наблюдатель будет отдаляться или приближаться к объекту наблюдения
Gl.glTranslated(0, 0, -7 -trackBar1.Value);
// 2 поворота (углы rot_1 и rot_2)
Gl.glRotated(rot_1, 1, 0, 0);
Gl.glRotated(rot_2, 0, 1, 0);
// устанавливаем размер точек, равный 5
Gl.glPointSize(5.0f);
// условие switch определяет установленный режим отображения, на основе выбранного пункта элемента
// comboBox, установленного в форме программы
switch (comboBox1.SelectedIndex)
{
case 0: // отображение в виде точек
{
// режим вывода геометрии - точки
Gl.glBegin( Gl.GL_POINTS);
// выводим всю ранее просчитанную геометрию объекта
for ( int ax = 0; ax < count_elements; ax++)
{
for ( int bx = 0; bx < Iter; bx++)
{
// отрисовка точки
Gl.glVertex3d(ResaultGeometric[ax, bx, 0], ResaultGeometric[ax, bx, 1], ResaultGeometric[ax, bx, 2]);
}
}
// завершаем режим рисования
Gl.glEnd();
break ;
}
case 1: // отображение объекта в сеточном режиме, используя режим GL_LINES_STRIP
{
// устанавливаем режим отрисовки линиями (последовательность линий)
Gl.glBegin( Gl.GL_LINE_STRIP);
for ( int ax = 0; ax < count_elements; ax++)
{
for ( int bx = 0; bx < Iter; bx++)
{
Gl.glVertex3d(ResaultGeometric[ax, bx, 0], ResaultGeometric[ax, bx, 1], ResaultGeometric[ax, bx, 2]);
Gl.glVertex3d(ResaultGeometric[ax + 1, bx, 0], ResaultGeometric[ax + 1, bx, 1], ResaultGeometric[ax + 1, bx, 2]);
if (bx + 1 < Iter - 1)
{
Gl.glVertex3d(ResaultGeometric[ax + 1, bx + 1, 0], ResaultGeometric[ax + 1, bx + 1, 1], ResaultGeometric[ax + 1, bx + 1, 2]);
}
else
{
Gl.glVertex3d(ResaultGeometric[ax + 1, 0, 0], ResaultGeometric[ax + 1, 0, 1], ResaultGeometric[ax + 1, 0, 2]);
}
}
}
Gl.glEnd();
break ;
}
case 2: // отрисовка оболочки с расчетом нормалей для корректного затенения граней объекта
{
Gl.glBegin( Gl.GL_QUADS); // режим отрисовки полигонов состоящих из 4 вершин
for ( int ax = 0; ax < count_elements; ax++)
{
for ( int bx = 0; bx < Iter; bx++)
{
// вспомогательные переменные для более наглядного использования кода при расчете нормалей
double x1 = 0, x2 = 0, x3 = 0, x4 = 0, y1 = 0, y2 = 0, y3 = 0, y4 = 0, z1 = 0, z2 = 0, z3 = 0, z4 = 0;
// первая вершина
x1 = ResaultGeometric[ax, bx, 0];
y1 = ResaultGeometric[ax, bx, 1];
z1 = ResaultGeometric[ax, bx, 2];
if (ax + 1 < count_elements) // если текущий ax не последний
{
// берем следующую точку последовательности
x2 = ResaultGeometric[ax + 1, bx, 0];
y2 = ResaultGeometric[ax + 1, bx, 1];
z2 = ResaultGeometric[ax + 1, bx, 2];
if (bx + 1 < Iter - 1) // если текущий bx не последний
{
// берем следующую точку последовательности и следующий медиан
x3 = ResaultGeometric[ax + 1, bx + 1, 0];
y3 = ResaultGeometric[ax + 1, bx + 1, 1];
z3 = ResaultGeometric[ax + 1, bx + 1, 2];
// точка, соответствующая по номеру, только на соседнем медиане
x4 = ResaultGeometric[ax, bx + 1, 0];
y4 = ResaultGeometric[ax, bx + 1, 1];
z4 = ResaultGeometric[ax, bx + 1, 2];
}
else
{
// если это последний медиан, то в качестве след. мы берем начальный (замыкаем геометрию фигуры)
x3 = ResaultGeometric[ax + 1, 0, 0];
y3 = ResaultGeometric[ax + 1, 0, 1];
z3 = ResaultGeometric[ax + 1, 0, 2];
x4 = ResaultGeometric[ax, 0, 0];
y4 = ResaultGeometric[ax, 0, 1];
z4 = ResaultGeometric[ax, 0, 2];
}
}
else // данный элемент ax последний, следовательно, мы будем использовать начальный (нулевой) вместо данного ax
{
// следующей точкой будет нулевая ax
x2 = ResaultGeometric[0, bx, 0];
y2 = ResaultGeometric[0, bx, 1];
z2 = ResaultGeometric[0, bx, 2];
if (bx + 1 < Iter - 1)
{
x3 = ResaultGeometric[0, bx + 1, 0];
y3 = ResaultGeometric[0, bx + 1, 1];
z3 = ResaultGeometric[0, bx + 1, 2];
x4 = ResaultGeometric[ax, bx + 1, 0];
y4 = ResaultGeometric[ax, bx + 1, 1];
z4 = ResaultGeometric[ax, bx + 1, 2];
}
else
{
x3 = ResaultGeometric[0, 0, 0];
y3 = ResaultGeometric[0, 0, 1];
z3 = ResaultGeometric[0, 0, 2];
x4 = ResaultGeometric[ax, 0, 0];
y4 = ResaultGeometric[ax, 0, 1];
z4 = ResaultGeometric[ax, 0, 2];
}
}
// переменные для расчета нормали
double n1 = 0, n2 = 0, n3 = 0;
// нормаль будем рассчитывать как векторное произведение граней полигона
// для нулевого элемента нормаль мы будем считать немного по-другому.
// на самом деле разница в расчете нормали актуальна только для первого и последнего полигона на медиане
if (ax == 0) // при расчете нормали для ax мы будем использовать точки 1,2,3
{
n1 = (y2 - y1) * (z3 - z1) - (y3 - y1) * (z2 - z1);
n2 = (z2 - z1) * (x3 - x1) - (z3 - z1) * (x2 - x1);
n3 = (x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1);
}
else // для остальных - 1,3,4
{
n1 = (y4 - y3) * (z1 - z3) - (y1 - y3) * (z4 - z3);
n2 = (z4 - z3) * (x1 - x3) - (z1 - z3) * (x4 - x3);
n3 = (x4 - x3) * (y1 - y3) - (x1 - x3) * (y4 - y3);
}
// если не включен режим GL_NORMILIZE, то мы должны в обязательном порядке
// произвести нормализацию вектора нормали перед тем как передать информацию о нормали
double n5 = (double)Math.Sqrt(n1 * n1 + n2 * n2 + n3 * n3);
n1 /= (n5 + 0.01);
n2 /= (n5 + 0.01);
n3 /= (n5 + 0.01);
// передаем информацию о нормали
Gl.glNormal3d(-n1, -n2, -n3);
// передаем 4 вершины для отрисовки полигона
Gl.glVertex3d(x1, y1, z1);
Gl.glVertex3d(x2, y2, z2);
Gl.glVertex3d(x3, y3, z3);
Gl.glVertex3d(x4, y4, z4);
}
}
// завершаем выбранный режим рисования полигонов
Gl.glEnd();
break ;
}
}
// возвращаем сохраненную матрицу
Gl.glPopMatrix();
// завершаем рисование
Gl.glFlush();
// обновляем элемент AnT
AnT.Invalidate();
}
На рисунках 5, 6, 7 представлены результаты работы программы: вращающееся тело с различными режимами отрисовки геометрии.
Рисунок 5. Пример построения объекта в режиме точек.
Рисунок 6. Пример построения объекта в режиме линий.
Рисунок 7. Пример построения объекта в режиме полигонов.
В следующей главе мы рассмотрим создание 3D-объектов, реализация которых уже имеется в библиотеке GLUT, а также функции OpenGL для изменения их положения в пространстве и масштабировании без изменений в структуре геометрии объектов.