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

Новые статьи:

Баннеры:

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

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


1.1. Основные соглашения

1.2. Точка в пространстве

1.3. Создание буфера рисования

1.4. Программа генерации точек в пространстве

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

1.1. Основные соглашения

Для программирования трехмерной графики мы будем использовать интегрированную среду программирования Borland Delphi 7.0 (все примеры и программы написаны именно на ней). А вывод графики будем осуществлять через стандартный графический класс TCanvas, который присутствует практически у всех визуальных компонентов Delphi. Я не буду здесь описывать принципы работы в среде Borland Delphi и основ программирования на языке Object Pascal. Это отдельная и большая тема. Предполагается, что у вас уже есть опыт работы в Delphi, а также навыки программирования графики с помощью класса TCanvas. Если нет, то придется наверстать. Тем более что в интернете достаточно информации на эту тему.

Сразу скажу, что используя данный класс для вывода графики, нам не удаться добиться высокой скорости отрисовки объектов, и уж тем более сделать на нем свой DOOM 3. Но нам это и не нужно. Для изучения основ трехмерности он вполне пригоден. К тому же этот класс скрывает от нас множество ненужных проблем, которые у нас возникли бы, если бы мы выводили графику через низкоуровневые API – функции. Я уже не говорю про низкоуровневую работу с видеоадаптером. Вот там настоящий АД!

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

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

1.2. Точка в пространстве

Итак, что бы описать точку в пространстве, достаточно создать вот такую структуру:


// Структура, описывающая точку в пространстве

TPoint3D = record
   X: Real;
   Y: Real;
   Z: Real;
end;

На рисунке (1.2.1) видно, что каждая координата точки задает плоскость параллельную противоположенной координатной плоскости. Например, координата X – задает плоскости, которая параллельна координатной плоскости YZ. Пересечение всех трех плоскостей и дает нам искомое расположение точки в пространстве. На рисунке – это точка A.

 

Рис 1.2.1. Точка в пространстве

Итак, у нас имеются координаты точки в пространстве (X, Y, Z). Как же теперь нам отобразить эту точку на экране монитора? Очень просто! Для того что бы отобразить ее на мониторе, нам нужно спроецировать ее на плоскость экрана. Поэтому следующее, что нам понадобится – это формулы перевода трехмерных координат, в двухмерные, т.е. формулы проецирования.

Казалось бы, зачем нам какие-то формулы, когда можно просто взять (X, Y) – координаты точки, и по этим значениям нарисовать ее на экране? А координату Z – просто отбросить... В этом случае у нас получится параллельное (ортогональное) проецирование точки. Так конечно тоже можно сделать, и объекты, построенные на экране, таким образом, тоже будут казаться трехмерными. Но все-таки отображаться они буду не очень красиво. Дело в том, что построенное таким образом изображение, не будет учитывать эффекта перспективы . Такой вид проекции используется в основном в техническом черчении.

Поэтому в программировании трехмерной графики лучше использовать центральное (перспективное) проецирование.

Посмотрите на рисунки (1.2.2), (1.2.3), (1.2.3) и (1.2.5), на них видны основные отличия между параллельной и центральной проекциями.

 

Рис 1.2.2. Параллельная (ортогональная) проекция

 

Рис 1.2.3. Параллельная (ортогональная) проекция трехмерного куба

 

Рис 1.2.4. Центральная (перспективная) проекция

 

Рис 1.2.5. Центральная (перспективная) проекция трехмерного куба
  • Красные линии на рисунках – лучи проецирования.
  • Синие точки – точки проецируемого объекта c координатами: (X, Y, Z).
  • Красные точки – проекции синих точек на плоскость экрана.

По рисункам 1.2.3. и 1.2.5. видно, что в первом случае противоположенные ребра у куба параллельны, а во втором, все ребра куба имеют точку схода где-то на горизонте.

Мы уже выяснили, что найти параллельную проекцию трехмерного объекта, не составляет большого труда, а вот с центральной проекцией будет немного посложнее. Ниже представлена формула нахождения центральной проекции точки (X, Y, Z).

k := D / (Z + OfsZ); 

x2d := oX + ROUND(X * k);
y2d := oY + ROUND(Y * k);

Давайте теперь разбираться в этих формулах. Начнем с простого. oX и oY – координаты центра нашего экрана. Что бы его вычислить, нужно проделать вот такую нехитрую операцию:


// Вычисляем центр экрана

oX := ClientWidth div 2;
oY := ClientHeight div 2;

ClientWidth, ClientHeight – это стандартные свойства формы, задающие ширину и высоту клиентской части окна, т.е. без учета системного меню, строки заголовка и т.д. Собственно в этой области мы и будем, выводит графику.

Следующее, это параметр OfsZ – задающий расстояние от центра локальной координатной оси проецируемой точки, до центра плоскости экрана. Что бы понять это взгляните на Рис 1.2.6.

Рис 1.2.6. Перспективная проекция

На рисунке, параметр Ofs показывает длину отрезка зеленого цвета, соединяющий центр плоскости экрана и центр локальной координатной оси.

Следующий параметр D – влияющий на коэффициент угла обзора проекции, который вычисляется по формуле: k := D / (Z + Ofs);

Нам важно научится правильно подбирать параметры D и Ofs для того что бы наша проекция отображалась корректно. На Рис 1.2.7. – показана проекция человеческой головы с неправильно подобранными параметрами D и Ofs. На Рис 1.2.8. параметры подобраны, верно.

Рис 1.2.7. Неправильная проекция

 

Рис 1.2.8. Правильная проекция

Т.е. по этим рисункам видно, к каким искажениям приводит неправильный подбор параметров. Дело в том, что наша проекция работает по принципу объектива видеокамеры. И в соответствии с законами оптики, для получения резкого изображения, проектируемый объект должен располагаться на определенном расстоянии от центра проекции (объектива видеокамеры). В наших формулах это расстояние задает параметр Ofs. С параметром D – немного посложнее. Как уже и было сказано, этот параметр задает коэффициент угла обзора проекции. Т.е. с помощью этого параметра можно производить масштабирование объектов.

Вы можете пока не заморачиваться с подбором этих параметров, и присваивать им следующие значения:


D   := (ClientWidth / ClientHeight) div 2;
Ofs := (ClientWidth / ClientHeight) div 2;

Вот и все! Использование такого присвоения приводит к более-менее нормальным результатам отображения объектов. Вы можете сами поэкспериментировать с подборами этих параметров с помощью моей программы: Projection. И увидеть, как меняется проекция объекта в зависимости от значений данных переменных.

Конечно, что бы получить максимально точную проекцию объекта, нужно провести более сложные расчеты. Но об этом мы поговорим чуть попозже.

1.3. Создание буфера рисования

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

Я назвал этот метод фундаментальным, потому что он применяется во всех программах активно работающих с компьютерной графикой, а в компьютерных играх и подавно…

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

Создать данный буфер в Delphi – проще пареной репы! Для этого достаточно использовать стандартный графический класс Delphi – TBitMap.

Создание происходит следующим образом:

DrawBuffer := TBitMap.Create;
DrawBuffer.Width := ClientWidth;
DrawBuffer.Height := ClientHeight;

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

Удаление:


if DrawBuffer <> nil then
begin
   DrawBuffer.Free;
   DrawBuffer := nil;
end;

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

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

Очистка происходит следующим образом:


procedure TMainForm.ClearBuffer;
begin
   DrawBuffer.Canvas.Pen.Color := BufferColor;
   DrawBuffer.Canvas.Brush.Color := BufferColor;
   DrawBuffer.Canvas.Rectangle(0, 0, ClientWidth, ClientHeight);
end;

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

DrawBuffer.Canvas.Pen.Color – цвет рамки нашего прямоугольника

DrawBuffer.Canvas.Brush.Color – цвет заливки прямоугольника

В нашем случае, цвет рамки и цвет заливки у нас одинаковы.

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

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


procedure TMainForm.FormResize(Sender: TObject);
begin

   if DrawBuffer <> nil then
   begin

     oX := ClientWidth div 2;
     oY := ClientHeight div 2;

     DrawBuffer.Width := ClientWidth;
     DrawBuffer.Height := ClientHeight;

   end;
end;

Если размеры нашего окна будут статичны, т.е. свойства формы BorderStyle будет иметь значения bsSingle или bsDialog, то данную функцию можно опустить.

Вот в принципе и все! Но остался еще один нюанс. Проведите небольшой эксперимент. Киньте на форму компонент Timer, свойству Enabled присвоим значение True, а значение Interval сделаем равным единице.

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


procedure TMainForm.Timer1Timer(Sender: TObject);
begin
   RePaint;
end;

Запустите приложение и посмотрите что будет. А будет очень некрасиво, окно будет страшно мерцать. Это происходит потому, что при перерисовки окна, ему сначала посылается сообщение ”WM_ERASEBKGND”, которая стирает фон, а только потом посылается сообщение "WM_PAINT". Поэтому, что бы убрать это неприятное мерцание, нам просто нужно перекрыть функцию, которая вызывается при получении окном сообщения ”WM_ERASEBKGND”. Сделать это, тоже очень легко.

Достаточно добавить в раздел private главной формы следующую функцию:


procedure WMErasebkgnd(var Msg: TWMErasebkgnd); message WM_ERASEBKGND;

А само описание функции оставить пустым:


procedure TMainForm.WMErasebkgnd(var Msg: TWMErasebkgnd); 
begin

end;

1.4. Программа генерации точек в пространстве

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

Мы напишем программу, которая генерирует заданное количество точек со случайными координатами (X, Y, Z). Что бы показать, что эти точки действительно находятся в пространстве, мы будем вращать их вокруг локальной системы координат, к которой они принадлежат, и проецировать на экран (на окно приложения).

Рис 1.4.1. Окно программы

Процедура генерации точек выглядит следующим образом:


Randomize;

for I := 1 to PointsCount do
begin
   Points3D[I].X := Random(ClientWidth) - oX;
   Points3D[I].Y := Random(ClientHeight) - oY;
   Points3D[I].Z := Random(ClientWidth) - oX;
end;

Из этой процедуры видно, что точки генерируются так, что бы их координаты не выходили из поля обзора нашей проекции. Points3D – массив структур TPoint3D. PointsCount – количество элементов в массиве (т.е. количество генерируемых точек).


const
   PointsCount = 1000;

var
   Points3D: array[1..PointsCount] of TPoint3D;

Теперь напишем главную процедуру рисования точек. Мы будем вызывать ее тогда, когда наше окно нужно будет перерисовать. Например, когда выполняется событие формы OnPaint, OnResize или событие таймера OnTimer.


procedure TMainForm.DrawPoints; 

var
  tX: Real;
  tY: Real;
  tZ: Real;
  Sum: Real;
  X: Integer;
  Y: Integer;
  I: Integer; 

begin


  if DrawBuffer <> nil then
  begin
  ClearBuffer;


   for I := 1 to PointsCount do
   begin


    case Direct of
      TRUE: begin
      tY := Points3D[I].Y * CosA - Points3D[I].Z * SinA;
      tZ := Points3D[I].Y * SinA + Points3D[I].Z * CosA;
      Points3D[I].Y := tY;
      Points3D[I].Z := tZ; 
      end;

      FALSE: begin
      tX := Points3D[I].X * CosA - Points3D[I].Z * SinA;
      tZ := Points3D[I].X * SinA + Points3D[I].Z * CosA;
      Points3D[I].X := tX;
      Points3D[I].Z := tZ;
      end; 


    end; 


    SUM := D / (POINTS3D[I].Z + Ofs);

    X := oX + ROUND(POINTS3D[I].X * SUM);
    Y := oY + ROUND(POINTS3D[I].Y * SUM);

    DrawBuffer.Canvas.Pen.Color := clWhite;
    DrawBuffer.Canvas.Brush.Color := clWhite;

    DrawBuffer.Canvas.Ellipse(X - rPoint, Y - rPoint, X + 
    rPoint, Y + rPoint); 


   end;


   Canvas.Draw(0, 0, DrawBuffer); 
   end; 

end;

Блок-схема этой процедуры будет выглядеть следующим образом:

Рис 1.4.2. Блок-схема процедуры рисования точек

Как видно, в принципе ничего сложного тут нет... У нас остались не разобраны только формулы поворота точек вокруг координатной оси. В программе поворот точек у нас происходит следующим образом :


Alpha := 0.01;

CosA := Cos(Alpha);
SinA := Sin(Alpha);

case Direct of

  TRUE: begin 
   tY := Points3D[I].Y * CosA - Points3D[I].Z * SinA;
   tZ := Points3D[I].Y * SinA + Points3D[I].Z * CosA;
   Points3D[I].Y := tY;
   Points3D[I].Z := tZ;
  end;

  FALSE: begin 
   tX := Points3D[I].X * CosA - Points3D[I].Z * SinA;
   tZ := Points3D[I].X * SinA + Points3D[I].Z * CosA;
   Points3D[I].X := tX;
   Points3D[I].Z := tZ;
  end;

end;

Переменная Direct задает направление поворота. Если Direct = True, то точки вертятся сверху вниз, если Direct = False, то слева направо. Подробно эти формулы мы сейчас разбирать не будем, а познакомимся с ними в следующих наших уроках. Но уже сейчас видно, что ничего сложного в них тоже нет…





23Июня2017|Никита
Отличная статья, спасибо!
Поможет мне в написании универсального визуализатора музыки
Сообщение № 10
08Октября2016|El
спасибо! хорошо изложено, интересно, кое-что даже стало понятно, как новичку в этом деле.)
Сообщение № 9
26Мая2016|Виталя
Спасибо, мне очень понравился подход - от простого к сложному.
Сообщение № 8
05Июня2015|ujif
обещали про коэффициент D рассказать
и де?
Сообщение № 7
23Апреля2013|pragma
lol.
Потестил проц Intel Core i7.
Нормально вытягивает 15000 - 17000 точек в один пиксель(с увеличением размера точки fps падает).

В диспетчере максимальная нагрузка на проц(какой нить один) начинается где то с 6000 - 6500 точек.
Сообщение № 6
23Апреля2013|pragma
Здравствуйте, есть несколько вопросов.
1. Как вы импортировали меш башки в свой проект?
2. Рисовали ее с помощью GDI?
3. Я вот думаю повысится ли производительность, если заюзать ассемблерные вставки, в проекте на VC++ 2008 при пересчете и отрисовке меша? Понимаю, что под винду лучше юзать d3d, ogl, чисто спортивный интерес.
Сообщение № 5
12Мая2012|n3ytron
Большое спасибо!!! Очень помогло. Понятно и интересно. Продолжайте писать такие статьи-у вас очень хорошо выходит ^__^
Сообщение № 4
21Октября2011|alex_ey
К сожалению, исходники я посеял. Но в новой свой статье, я сделаю тоже что-нибудь подобное.
Сообщение № 3
20Октября2011|Дмитрий
Отличная статья, Спасибо!
А можете выложить исходники программы Projection?
очень интересно)
Сообщение № 2
11Июля2011|Mauni
Спасибо огромное за статью!
А будет ли продолжение в виде второй части?
Сообщение № 1
имя / ник:

e-mail:

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

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

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

 


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