Персональная страничка/блог программиста из сибири.

Статьи:

Баннеры:

Статьи
Подписаться на RSS.
  Поиск

Программирование трехмерной графики. Часть 3.

3.1. Полигоны.

3.2. Сортировка полигонов.

3.3. Освещение.

В предыдущих статьях мы познакомились с основами программирования трехмерной графики. Научились строить трехмерные точки и проецировать их на экран нашего монитора. А также научились производить с ними основные операции по перемещению в их пространстве.

3.1. Полигоны.

Вершины (точки в пространстве) – это основа (каркас) всех трехмерных объектов. Из вершин собираются полигоны, а из полигонов – сложные трехмерные объекты.

Что же такое полигон? Взгляните на рисунок (3.1.1.).

Рис 3.1.1. Виды полигонов.

Т.е. по рисунку видно, что полигон – это геометрическая фигура, состоящая из отрезков, последовательно соединенных своими концами в замкнутую фигуру. Треугольник, квадрат, прямоугольник являются частными случаями полигона.

Полигон (polygon), в переводе с английского – это многоугольник. Просто у нас термин “многоугольник” – используются в основном в геометрии. А термин “полигон” прикрепился именно к понятиям трехмерной графики. А по сути - это одно и тоже.

Что бы определить наш треугольник создадим такую структуру:


  TVertex = record
    X: Double;
    Y: Double;
    Z: Double;
  end;

  TTriangle = array[0..2] of TVertex;

Структура TVertex описывает точку в пространстве, а массив TTriangle – описывает наш полигон. Для построения треугольника, достаточно трех точек. Поэтому наш массив состоит из трех элементов, которые определяют координаты вершин треугольника.

Теперь давайте определим наш, треугольник. Для этого создадим переменную типа TTriangle, и зададим ей следующие координаты:


var
  LTriangle: TTriangle;
begin
  // Первая вершина
  LTriangle[0].X := 0;
  LTriangle[0].Y := 100;
  LTriangle[0].Z := 0;

  // Вторая вершина
  LTriangle[1].X := - 100;
  LTriangle[1].Y := - 100;
  LTriangle[1].Z := 0;

  // Третья вершина
  LTriangle[2].X := 100;
  LTriangle[2].Y := - 100;
  LTriangle[2].Z := 0;
end;

Координаты у нас задаются следующим образом: т.к. точка (0,0,0) – это центр экрана, то первая вершина у нас лежит на оси oY и имеет значение Y = 100; вторая вершина лежит в левой нижней части графика и имеет координаты: X = -100; Y = - 100; третья точка соответственно лежит напротив второй и имеет координаты: X = 100; Y = - 100.

Как видно, координату Z мы не учитываем (присваиваем ей ноль), т.е. наш треугольник будет параллелен координатной плоскости XY. См. рисунок (3.1.2.).

Рис 3.1.2. Треугольник.

В Delphi у класса Canvas – есть замечательная функция рисования полигонов.


Canvas.Polygon(const Points: array of TPoint);

В качестве параметра мы передаем этой функции массив точек (TPoint), а функция соединяет все эти точки в полигон. Кол-во точек в массиве может быть не ограничено.

Если вы программирует в какой-нибудь другой среде, то можно воспользоваться WinAPI – функцией рисования полигона.


Polygon(DC: HDC; const Points; Count: Integer): BOOL; 

В качестве параметров, в эту функцию передается графический контекст устройства “DC”, на котором будет происходить вывод графики, массив точек “Points”, и количество этих точек “Count”.

Теперь рассмотрим функцию рисования нашего треугольника:


var
  i: Integer;
  LSum: Double;
  tY, tZ: Double;
begin
  // Очищаем буфер рисования
  ClearBuffer;

  // Поворачиваем вершины нашего треугольника,
  // вокруг центра координатной плоскости XY.
  for i := 0 to 2 do
  begin
    tY := FTriangle[I].X * CosA - FTriangle[I].Y * SinA;
    tZ := FTriangle[I].X * SinA + FTriangle[I].Y * CosA;
    FTriangle[I].X := tY;
    FTriangle[I].Y := tZ;
  end;

  // Переводим трехмерные координаты в двухмерные.
  // Заполняем массив точек FPolygon.
  for i := 0 to 2 do
  begin
    LSum := D / (FTriangle[i].Z + Ofs);
    FPolygon[i].X := oX + ROUND(FTriangle[i].X * LSum);
    FPolygon[i].Y := oY - ROUND(FTriangle[i].Y * LSum);
  end;

  // Рисуем наш полигон.
  DrawBuffer.Canvas.Pen.Color   := clWhite;
  DrawBuffer.Canvas.Brush.Color := clGreen;
  DrawBuffer.Canvas.Polygon(FPolygon);

  // Скидываем буфер рисования на экран.
  Canvas.Draw(0, 0, DrawBuffer);
end;

Тут все просто. Сначала мы очищаем экран. Потом совершаем поворот трех точек нашего треугольника вокруг координатной оси. (Формулы поворота были подробно разобраны во второй статье). Потом получившиеся точки переводим в двухмерные, и соответственно эти точки соединяем в полигон. Ниже Вы можете скачать пример рисования нашего треугольника.


Простейший полигон рисовать мы уже научились. Теперь давайте нарисуем что-нибудь посложнее. Сейчас мы нарисуем не просто один полигон, а целую трехмерную фигуру. Это конечно же будет куб. Так как эта простая фигура, и на ней легко показать принципы построения трехмерных объектов.

Для построения квадрата нам потребуется четыре вершины, поэтому объявляем тип TQuad – следующим образом:


type
  TQuad = array[0..3] of TVertex;

У куба шесть сторон, поэтому нам потребуется шесть квадратов, т.е. шесть переменных типа TQuad. Объявляем этот массив в программе:


var
  FQuads: array[0..5] of TQuad;

Первый индекс в двухмерном массиве определяет номер квадрата, второй индекс – номер вершины данного квадрата.

Теперь для всех шести квадратов нам нужно задать их координаты в пространстве, так же как мы делали для треугольника.


  // Лицевая сторона
  FQuads[0][0].X := - 100;
  FQuads[0][0].Y :=   100;
  FQuads[0][0].Z :=   100;

  FQuads[0][1].X :=   100;
  FQuads[0][1].Y :=   100;
  FQuads[0][1].Z :=   100;

  FQuads[0][3].X := - 100;
  FQuads[0][3].Y := - 100;
  FQuads[0][3].Z :=   100;

  FQuads[0][2].X :=   100;
  FQuads[0][2].Y := - 100;
  FQuads[0][2].Z :=   100;

  // Задняя сторона
  FQuads[1][0].X := - 100;
  FQuads[1][0].Y :=   100;
  FQuads[1][0].Z := - 100;

  FQuads[1][1].X :=   100;
  FQuads[1][1].Y :=   100;
  FQuads[1][1].Z := - 100;

  FQuads[1][3].X := - 100;
  FQuads[1][3].Y := - 100;
  FQuads[1][3].Z := - 100;

  FQuads[1][2].X :=   100;
  FQuads[1][2].Y := - 100;
  FQuads[1][2].Z := - 100;

  // Верхняя сторона
  FQuads[2][0].X := - 100;
  FQuads[2][0].Y :=   100;
  FQuads[2][0].Z :=   100;

  FQuads[2][1].X :=   100;
  FQuads[2][1].Y :=   100;
  FQuads[2][1].Z :=   100;

  FQuads[2][2].X :=   100;
  FQuads[2][2].Y :=   100;
  FQuads[2][2].Z := - 100;

  FQuads[2][3].X := - 100;
  FQuads[2][3].Y :=   100;
  FQuads[2][3].Z := - 100;

  // Нижняя сторона
  FQuads[3][0].X := - 100;
  FQuads[3][0].Y := - 100;
  FQuads[3][0].Z :=   100;

  FQuads[3][1].X :=   100;
  FQuads[3][1].Y := - 100;
  FQuads[3][1].Z :=   100;

  FQuads[3][2].X :=   100;
  FQuads[3][2].Y := - 100;
  FQuads[3][2].Z := - 100;

  FQuads[3][3].X := - 100;
  FQuads[3][3].Y := - 100;
  FQuads[3][3].Z := - 100;

  // Левая сторона
  FQuads[4][0].X := - 100;
  FQuads[4][0].Y :=   100;
  FQuads[4][0].Z :=   100;

  FQuads[4][1].X := - 100;
  FQuads[4][1].Y :=   100;
  FQuads[4][1].Z := - 100;

  FQuads[4][2].X := - 100;
  FQuads[4][2].Y := - 100;
  FQuads[4][2].Z := - 100;

  FQuads[4][3].X := - 100;
  FQuads[4][3].Y := - 100;
  FQuads[4][3].Z :=   100;

  // Правая сторона
  FQuads[5][0].X :=   100;
  FQuads[5][0].Y :=   100;
  FQuads[5][0].Z :=   100;

  FQuads[5][1].X :=   100;
  FQuads[5][1].Y :=   100;
  FQuads[5][1].Z := - 100;

  FQuads[5][2].X :=   100;
  FQuads[5][2].Y := - 100;
  FQuads[5][2].Z := - 100;

  FQuads[5][3].X :=   100;
  FQuads[5][3].Y := - 100;
  FQuads[5][3].Z :=   100;

Получился довольно таки большой кусок кода инициализации куба. Но тут ничего сложного нет. Мы задаем координаты вершин каждой стороне куба: верхней, нижней, задней и т.д…

И наконец, осталось немного переделать нашу процедуру рисования графики. Получится следующее:


  // Очищаем буфер рисования
  ClearBuffer;

  // Поворачиваем вершины наших квадратов
  for i := 0 to QuadsCount - 1 do
  begin
    for j := 0 to 3 do
    begin
      tX := FQuads[i][j].X * CosA - FQuads[i][j].Y * SinA;
      tY := FQuads[i][j].X * SinA + FQuads[i][j].Y * CosA;
      FQuads[i][j].X := tX;
      FQuads[i][j].Y := tY;

      tX := FQuads[i][j].X * CosA - FQuads[i][j].Z * SinA;
      tZ := FQuads[i][j].X * SinA + FQuads[i][j].Z * CosA;
      FQuads[i][j].X := tX;
      FQuads[i][j].Z := tZ;
    end;
  end;

  // Переводим трехмерные координаты в двухмерные.
  // Заполняем массив точек FPolygon.
  for i := 0 to QuadsCount - 1 do
  begin
    for j := 0 to 3 do
    begin
      LSum := D / (FQuads[i][j].Z + Ofs);
      FPolygon[j].X := oX + ROUND(FQuads[i][j].X * LSum);
      FPolygon[j].Y := oY - ROUND(FQuads[i][j].Y * LSum);
    end;
    
    // Рисуем наш полигон.
    DrawBuffer.Canvas.Pen.Color   := clGreen;
    DrawBuffer.Canvas.Brush.Color := clGreen;
    DrawBuffer.Canvas.Polygon(FPolygon);
  end;

  // Скидываем буфер рисования на экран.
  Canvas.Draw(0, 0, DrawBuffer);

Вместо одного треугольника, нам теперь нужно нарисовать шесть квадратов. Поэтому проходимся по массиву FQuads, и выполняем поворот для всех вершин входящих в каждый квадрат. Для наглядности мы поворачиваем вершины сначала по оси XY, а потом по XZ.

Потом переводим координаты каждого квадрата в двухмерное представление, и по очереди рисуем их с помощью функции Polygon. Т.е. в эту функцию уже пихается массив из четырех точек, а не из трех, когда мы рисовали треугольник.


3.2. Сортировка полигонов.

Теперь попробуем немного модифицировать нашу программу. Давайте в процедуре рисования графики, изменить цвет рамки наших квадратов с зеленого на белый.


DrawBuffer.Canvas.Pen.Color := clWhite;

Запустите программу, и вы увидите, что получилось не очень красиво. Дело в том, что наши полигоны рисуются в неправильной последовательности. Например, задний полигон вдруг выходит на передний план, хотя на самом деле он должен рисоваться самым первым. Т.е. самые отдаленные от экрана полигоны должны рисоваться самыми первыми, а более близкие к экрану, последними. При рисовании более близкие полигоны должны накладываться на уже нарисованные более отдаленные полигоны, тогда трехмерная сцена будет отображаться верно. Для этого и существует метод сортировки полигонов, от более отдаленного от экрана, до самого близкого. Или по другому его еще называют – удаление невидимых граней.

Геометрически это непростая задача. Во всех видеокартах, задача удаления невидимых граней уже давно решена на аппаратном уровне. Там используется Z-буффер. У нас же нет доступа к видеокарте, и мы рисуем стандартными функциями Window для вывода графики. Поэтому нам придется решать эту задачу геометрически.

К счастью, куб – очень простая фигура. Поэтому отсортировать его полигоны нам будет очень просто.

Для начала немного модифицируем наши типы данных:


  // Вершина (трехмерная точка)
  TVertex = record
    X: Double;
    Y: Double;
    Z: Double;
  end;

  // Вектор
  TVector = TVertex;

  // Квадрат
  TQuad = record
    Color: TColor;
    Normal: TVector;
    Point2D: TPoint;
    Vertex: array[0..3] of TVertex;
  end;

Мы добавили новый тип - TVector. Это-то же самое, что и TVertex. Этот тип мы добавили просто для удобства программирования. Так же мы изменили тип TQuad. Теперь это не просто массив, а структура. Сюда вошли три новых поля. Color – цвет полигона. Point2D –двухмерная проекция нашей вершины. Теперь мы будем после перевода вершины в 2D, сразу заносить их в это поле. Normal – нормаль полигона.

На последнем остановимся поподробнее. Что такое нормаль, и зачем она нужна? Во-первых, нормаль – это перпендикуляр. Т.е. в нашем случае перпендикуляр к нашему полигону. Во-вторых – это вектор. И в-третьих, длинна этого вектора всегда равна единице.

В трехмерной графике нормали используются в основном для просчета освещения объектов. Т.е., чем больше угол между нормалью и вектором света, тем больше будет освещен наш полигон. А приводить эту нормаль к единичной длине нужно для упрощения математических вычислений во время прорисовки графики.

Допустим, у нас есть два вектора A1(x1, y1, z1) и B1(x2, y2, z2). Формула нахождения угла между ними выглядит следующим образом:

Т.е. скалярное произведения векторов нужно разделить на произведение длин этих векторов. Но так как длинны векторов у нас равны единице, то нижнюю часть формулы можно легко отбросить. Получится вот такая простая формула:

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

При инициализации куба, каждая его сторона будет перпендикулярна одной из оси координат, поэтому можно задать следующие координаты нормалей:


  // Лицевая сторона
  FQuads[0].Normal.X := 0;
  FQuads[0].Normal.Y := 0;
  FQuads[0].Normal.Z := 1;

  // Задняя сторона
  FQuads[1].Normal.X := 0;
  FQuads[1].Normal.Y := 0;
  FQuads[1].Normal.Z := - 1;

  // Верхняя сторона
  FQuads[2].Normal.X := 0;
  FQuads[2].Normal.Y := 1;
  FQuads[2].Normal.Z := 0;

  // Нижняя сторона
  FQuads[3].Normal.X := 0;
  FQuads[3].Normal.Y := - 1;
  FQuads[3].Normal.Z := 0;

  // Левая сторона
  FQuads[4].Normal.X := - 1;
  FQuads[4].Normal.Y := 0;
  FQuads[4].Normal.Z := 0;

  // Правая сторона
  FQuads[5].Normal.X := 1;
  FQuads[5].Normal.Y := 0;
  FQuads[5].Normal.Z := 0;

С помощью этих нормалей мы и будем сортировать наши полигоны. А точнее по координате Z – каждой нормали. Т.е. полигоны с самым меньшим значением будут рисоваться самыми первыми, а полигоны с большими значениями этой координаты, рисуются последними.

Это самый простой способ сортировки полигонов, но и подходит он только для простых объектов. Куба, сферы и других выпуклых фигур. Для трехмерных моделей с уже более-менее сложной структурой этот способ уже работать не будет. В таких случаях нам придется использовать сложные алгоритмы удаления невидимых граней, например - алгоритм художника.

Сортировать наши полигоны мы будем с помощью метода быстрой сортировки. Когда у нас полигонов немного, в нашем случае их шесть – этот метод не дает ощутимой прибавки в производительности. Но вот когда число их зашкаливает за тысячу, выигрыш в быстродействии будет уже ощутимый. Поэтому будем использовать только эту сортировку.

Теперь рассмотрим процедуру рисования нашего куба:


  // Очищаем буфер рисования
  ClearBuffer;

  // Поворачиваем вершины наших квадратов
  for i := 0 to QuadsCount - 1 do
  begin
    for j := 0 to 3 do
    begin
      // Поворачиваем вершины
      tX := FQuads[i].Vertex[j].X * CosA - FQuads[i].Vertex[j].Y * SinA;
      tY := FQuads[i].Vertex[j].X * SinA + FQuads[i].Vertex[j].Y * CosA;
      FQuads[i].Vertex[j].X := tX;
      FQuads[i].Vertex[j].Y := tY;

      tX := FQuads[i].Vertex[j].X * CosA1 - FQuads[i].Vertex[j].Z * SinA1;
      tZ := FQuads[i].Vertex[j].X * SinA1 + FQuads[i].Vertex[j].Z * CosA1;
      FQuads[i].Vertex[j].X := tX;
      FQuads[i].Vertex[j].Z := tZ;
    end;

    // Поворачиваем нормали
    tX := FQuads[i].Normal.X * CosA - FQuads[i].Normal.Y * SinA;
    tY := FQuads[i].Normal.X * SinA + FQuads[i].Normal.Y * CosA;
    FQuads[i].Normal.X := tX;
    FQuads[i].Normal.Y := tY;

    tX := FQuads[i].Normal.X * CosA1 - FQuads[i].Normal.Z * SinA1;
    tZ := FQuads[i].Normal.X * SinA1 + FQuads[i].Normal.Z * CosA1;
    FQuads[i].Normal.X := tX;
    FQuads[i].Normal.Z := tZ;
  end;

  // Переводим трехмерные координаты в двухмерные.
  // Заполняем массив точек FPolygon.
  for i := 0 to QuadsCount - 1 do
  begin
    for j := 0 to 3 do
    begin
      LSum := D / (FQuads[i].Vertex[j].Z + Ofs);
      FQuads[i].Point2D[j].X := oX + ROUND(FQuads[i].Vertex[j].X * LSum);
      FQuads[i].Point2D[j].Y := oY - ROUND(FQuads[i].Vertex[j].Y * LSum);
    end;
    
  end;

  // Сортируем полигоны
  QuickSort(0, QuadsCount - 1);

  // Рисуем наши полигоны.
  for i := 0 to QuadsCount - 1 do
  begin
    DrawBuffer.Canvas.Pen.Color   := FQuads[i].Color;
    DrawBuffer.Canvas.Brush.Color := FQuads[i].Color;
    DrawBuffer.Canvas.Polygon(FQuads[i].Point2D);
  end;

  // Скидываем буфер рисования на экран.
  Canvas.Draw(0, 0, DrawBuffer);

Как видно процедура рисования куба у нас уже значительно усложнилась. Теперь мы выполняем поворот не только вершин куба, но и все его нормали. Т.е. нормаль полигон у нас должна вращаться в туже сторону, что и сам полигон.

Затем переводим все наши вершины в двухмерный вид. Т.е. заполняем массив Points2D – каждого полигона. Потом сортируем полигоны по координате Z. И наконец выводим их на экран.


3.3. Освещение.

Так как расчет нормалей в нашей программе мы уже реализовали, то добавить освещение к нашему кубу уже не составит больших проблем. Для начала давайте превратим наш тип TQuad в полноценный класс.


  TQuad = class
  private
    FRGB: TRGB;
    procedure SetColor(const AColor: TColor);
    function GetColor: TColor;
  public
    Normal: TVector;
    Point2D: array[0..3] of TPoint;
    Vertex: array[0..3] of TVertex;

    property Color: TColor read GetColor write SetColor;
  end;

Т.к. TQuad – теперь стал классом, то перед началом работы с ним, нам сначала нужно будет создать объекты этого класса, что бы с ними работать. Т.е. создать шесть полигонов. Делается это просто:


  FQuads[0] := TQuad.Create;
  FQuads[1] := TQuad.Create;
  FQuads[2] := TQuad.Create;
  FQuads[3] := TQuad.Create;
  FQuads[4] := TQuad.Create;
  FQuads[5] := TQuad.Create;

А в событии уничтожения формы нам нужно освободить эти объекты. Делается это тоже просто:


var
  i: Integer;
begin
  for i := 0 to QuadsCount - 1 do
  begin
    FQuads[i].Free;
  end;
end;

Теперь у нас поле Color – является не просто переменной класса, теперь она является свойством класса. При попытки присвоить этому полю какой-либо значение, т.е. присвоить ему цвет – будет вызываться процедура SetColor. А при попытки извлечь из этого поля значение цвета, будет соответственно вызываться функция GetColor.

Вот реализация этих методов:


procedure TQuad.SetColor(const AColor: TColor);
begin
  FRGB.R := GetRValue(AColor);
  FRGB.G := GetGValue(AColor);
  FRGB.B := GetBValue(AColor);
end;


function TQuad.GetColor: TColor;
var
  LAngle: Double;
begin
  LAngle := - Normal.Z;
  Result := RGB(Round(FRGB.R * LAngle), 
                Round(FRGB.G * LAngle), 
                Round(FRGB.B * LAngle));
end;

Метод SetColor раскладывает наш цвет на RGB – составляющую. И присваивается приватной переменной класса FRGB. Приватная переменная (поле) – это переменная, которая будет доступна только внутри нашего класса (в методах нашего класса).

А вот в методе GetColor и происходит самой интересное. В этом методе мы RGB цвета конвертируем обратно в тип TColor. При этом предварительно умножая каждую составляющую цвета на его интенсивность. А интенсивность цвета у нас будет определяться переменной Normal.Z, т.е. Z – координатой нормали нашего полигона.

Почему так? Да очень просто! Договоримся, что источник света у нас будет направлен на экран монитора. Т.е. световые лучи будут направлены как бы из глаз человека сидящего перед монитором. Поэтому вектор светового луча будет параллелен координатной оси Z. Т.е. координаты вектора луча света будут следующие: (0, 0, 1);

Этот вектор тоже единичный. А как мы уже узнали, найти угол между двумя единичными векторами можно по формуле angle = X1X2 + Y1Y2 + Z1Z2.

Подставляем наши вектора в эту формулу и получаем: Normal.X * 0 + Normal.Y * 0 + Normal.Z * 1. Отсюда видно переменная Normal.Z и будет коэффициентом угла между нормалью полигона и вектором луча света. Все просто!

Чем больше значение Normal.Z будет приближаться к единице, тем меньше будет угол между нормалью и лучом света и тем соответственно больше будет интенсивность цвета.

Мы рассмотрели самый простейший вид освещения, и наверно это способ освещения единственный, который можно реализовать с помощью графических классов Delphi. Но я надеюсь, что этого было достаточно, что бы понять, по какому принципу в 3D графики рассчитываются источники света.




12Июня2016|ник
Спасибо! Нереально помог! Весь интернет перерыл ища как рисовать объемное изображение собственными ручками. Респект
Сообщение № 4
06Июня2015|юрий
непонятно стало в первой части где обещалось объяснить что такое D а потом уже университетская программа пошла , и ничего непонятно
Сообщение № 3
22Апреля2014|Валентина
+1
Мне тоже все 3 статьи понравилось. Все логично, понятно и доступно.
Спасибо!
Сообщение № 2
11Марта2013|Модя
Все три части понравились. Самое главное отличие от подобных статей – понятность. Спасибо.
Сообщение № 1
имя / ник:

e-mail:

Защита от спама:

Введите число, изображенное на картинке:

Текст комментария:

 


WWW.ALEXEYSPACE.RU
(c) alex_ey (Alexey Sokolov)
alex_ey@mail.ru