25th
Авг

Делаем динамические тени на OPENGL. Часть 1

Posted by Chas under Журнал, Статьи

аватар

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

Вадим Буренков
vadim_burenkov@mail.ru

Чтобы не тратить время на инициализацию OpenGL и избежать других проблем (например, с настройкой таймеров и рендера в текстуру) я буду использовать движок ZenGL [1]. Впрочем, от него нам многого не понадобится. Итак, приступим…

Инициализация OpenGL в ZenGL

Первым делом качаем ZenGL и создаем в нем простейшее приложение (вы можете найти его в папке LightEngine ресурсов статьи, там же вы найдете ZenGL):

код:

program LightEngine;
uses
   zgl_main,
   zgl_screen,
   zgl_window,
   zgl_timers,
   zgl_textures,
   zgl_textures_jpg,
   zgl_sprite_2d,
   zgl_mouse,
   zgl_keyboard,
   zgl_utils;
var
   BackTex:zglPTexture; // текстура фона
   bTiles:zglTTiles2D; // параметры тайлинга

procedure Init;
var n,j:integer;
begin
   // отключаем очищение буфера
   zgl_disable(COLOR_BUFFER_CLEAR );
   // Тут можно выполнять загрузку основных ресурсов
   // загрузка текстуры и настройка тайлинга
   BackTex:=tex_LoadFromFile( ‘Back.jpg’,0,TEX_DEFAULT_2D);
   // параметры тайлов фона
   bTiles.Count.X:=7;
   bTiles.Count.Y:=5;
   bTiles.Size.W:=128;
   bTiles.Size.H:=128;
   SetLength(bTiles.Tiles,7,5);
&nfor fto=0 to 4 do
&nbsforbsp;&ntob>for
j:=0 to 6 do bTiles.Tiles[j,n]:=1;
end;

procedure Draw;
begin
   // Тут «рисуем» что угодно :)
   tiles2d_Draw(BackTex,0,0,BTiles); // отрисовка фона
end;

procedure Update;
begin
   // Тут выполняется обработка данных
   if key_Press( K_ESCAPE ) then zgl_Exit;
   // обновление клавиш
   key_ClearState;
   Mouse_ClearState;
end;

procedure Timer;
begin
   // Будем в заголовке показывать количество кадров в секунду
   wnd_SetCaption( ‘LightEngine [ FPS: ' + u_IntToStr( zgl_Get(
   SYS_FPS ) ) + ' ]‘ );
end;

procedure Quit;
begin
   // Тут выполняется очищение данных
end;

Begin
   // Создаем таймер с интервалом 1000мс.
   timer_Add( @Timer, 1000 );
   // Создаем таймер с интервалом 10мс.
   timer_Add( @Update, 10 );
   // Регистрируем процедуру, что выполнится сразу после
   // инициализации ZenGL
   zgl_Reg( SYS_LOAD, @Init );
   // Регистрируем процедуру, где будет происходить рендер
    zgl_Reg( SYS_DRAW, @Draw );
    // Регистрируем процедуру, которая выполнится после завершения
    // работы ZenGL
    zgl_Reg( SYS_EXIT, @Quit );
    // Устанавливаем заголовок окна
    // Разрешаем курсор мыши
    wnd_ShowCursor( TRUE );
    // Указываем первоначальные настройки
    scr_SetOptions( 800, 600, REFRESH_MAXIMUM, FALSE, FALSE );
    // Инициализируем ZenGL
    zgl_Init;
End.

При инициализации мы указываем процедуры в которых будут производится различные действия (инициализация/обработка/очищение) а также параметры окна.

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

Другие непонятные процедуры можно посмотреть в справке, которая находится в папке doc движка. Чтобы при компиляции не возникло проблем необходимо указать расположение модулей движка в Project->Options- >Directories/Conditionals->SearchPath, а именно папки zengl/src и zengl/src/PasZLib (см. рис.1). Можно скомпилировать проект, увидеть вы должны следующее (см. рис.2).

рисунок 1

Рис. 1. Пути

рисунок 2

Рис. 2. Тайлинг

Немного теории

Теперь перейдем к теории вопроса. В движке мы должны реализовать два типа – источники света и объекты, которые отбрасывают тени (см. рис.3):

рисунок 3

Рис. 3. Тень от объекта

Источник света обладает параметрами:

  • положение
  • радиус
  • цвет
  • интенсивность

Все объекты являются невыпуклыми многоугольниками. Они имеют:

  • локальные координаты вершин
  • мировые координаты вершин
  • количество вершин
  • положение
  • угол поворота

Локальные координаты нужны, так как через положение и угол поворота объекта его можно разворачивать.

Для хранения данных об освещенности нам понадобятся два буфера размером в экран. Первый – альфа буфер. В него выводится круглый источник света (см. рис.4):

рисунок 4

Рис. 4. Источник света

После этого альфа буфер рисуется во второй буфер – буфер аккумуляции. При этом используется аддитивный режим блендинга, то есть цвета смешиваются. В буфере мы получаем такую картинку (см. рис.5):

рисунок 5

Рис. 5. Смешивание источников света

В нем светлые участки – там где свет, а темные – там где тьма. А теперь мы выводим буфер аккамуляции на экран с блендингом MULT. Получается так, что чем светлее цвет, тем он прозрачнее (см. рис.6):

рисунок 6

Рис. 6. Рисование с блендингом MULT

Как же делаются тени от объектов? При выводе источника света в альфа буфер на него рисуется форма тени черным цветом. Получается что от круга света «отрезают» кусок (см. рис.7):

рисунок 7

Рис. 7. Форма тени на свете

С помощью такого алгоритма получаются тени любой сложности, причем их количество, как и источников света с объектами неограниченно (см.рис.8).

Да будет свет!

Под следующий код сделаем модуль, который и будет отвечать за тени. Назовем его ZGLShadows.

рисунок 8

Рис. 8. Сцена с большим количеством теней

Напишем тип света:

код:

type
   PLightSource=^TLightSource;
   TLightSource=record
   position:leVect; // положение
   radius:single; // радиус
   color:TColorRGB; // цвет
   intensivity:single; // интенсивность cвета
   prev,next:PLightSource;
end;

Все данные будут храниться в “prev-next” (двухсвязных) списках (см. рис.9):

рисунок 9

Рис. 9. Списки

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

код:

Var
   // Источники света
   le_Lights:PLightSource; // список
   le_NumLights:integer; // количество

Более подробно о такой системе хранения данных можно почитать в Интернете. Как мы видим, у нас появились новые типы данных:

код:

type
   TColorRGB=record
   r,g,b:single;
end;

Данный тип нужен для хранения цвета в формате rgb, так как его использует OpenGL. ZenGL использует integer для хранения цветов (например $FFFFFF соответствует 1,1,1 в RGB), поэтому нам может понадобится процедура для перевода цвета в RGB:

код:

function IntToRGB(Color:Integer): TColorRGB;
begin
   Result.r := ((Color and $FF0000) shr 16) / 255;
   Result.g := ((Color and $FF00) shr 8) / 255;
   Result.b := (Color and $FF) / 255;
end;

Все координаты будут храниться в типе:

код:

type
   leVect=record
   x,y:single;
end;

Подробнее о нем будет написано позже, пока нам понадобится только формирование вектора по x и y:

код:

function le_v(x,y:single):leVect;
begin
   result.x:=x;
   result.y:=y;
end;

Перейдем к процедурам управления источниками света:

код:

Function le_CreateLightSource(p:leVect; radius,intensivity:single;
                                color:TColorRGB):PLightSource;
var t: PLightSource;
begin
   new(t);
   t.Next:= nil;
   t.Prev:= nil;
   t.position:=p;
   t.radius:=radius;
   t.intensivity:=intensivity;
   t.color:=color;
   t.Next:= le_Lights;
   if le_Lights <> nil then le_Lights.Prev:= t;
   le_Lights:= t;
   Result:= le_Lights;
   inc(le_NumLights);
end;

Функция создает источник света в памяти и возвращает указатель на него. Большую часть кода занимает работа со списками.

В следующей процедуре происходит рисование круга света как на рисунке 4. Он рисуется через GL_TRIANGLE_FAN. Первая точка в центре имеет цвет и интенсивность света, далее идут точки по радиусу окружности с нулевым цветом, благодаря чему мы имеем плавный переход цвета:

код:

procedure le_DrawLightSource(t:PLightSource);
var angle:single;
begin
   angle:=0;
   glBegin(GL_TRIANGLE_FAN);
   glColor4f(t.color.r, t.color.g, t.color.b, t.intensivity);
   glVertex2f(t.position.x,t.position.y);
   glColor4f(0, 0, 0, 0 );
   while angle<=Pi*2 do begin
     glVertex2f( t.radius*cos(angle) + t.position.x,
     t.radius*sin(angle) + t.position.y);
     angle:=angle+((PI*2)/le_numSubdivisions);
   end;
   glVertex2f(t.position.x+t.radius, t.position.y);
   glEnd();
end;

Количество треугольников, из которого рисуется круг, задается константой:

код:

const
   le_numSubdivisions = 32;

Следующая процедура рассчитывает и рисует тень для объектов, но о ней я напишу позже:

код:

procedure le_RenderShadowGeometry(t:PLightSource);

Далее напишем процедуру, которая освобождает память, занятую источником света:

код:

procedure le_FreeLightSource(t:PLightSource);
var DelT: PLightSource;
begin
   DelT:= t;
   if t.Prev <> nil then t.Prev.Next := t.Next
   else le_Lights:= t.Next;
   if t.Next <> nil then t.Next.Prev := t.Prev;
   Dispose(DelT);
   dec(le_NumLights);
   t:=nil
end;

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

код:

procedure le_EachLightSource(p:le_proc);
var t, tNext: PLightSource;
begin
   t:= le_Lights;
   while t <> nil do
   begin
     tNext:= t.Next;
     p(t);
     t:= tNext;
   end;
end;

le_proc – тип процедуры:

код:

type
   le_proc=procedure(d:Pointer);

Например, данная строчка нарисует все источники света:

код:

le_EachLightSource(@le_DrawLightSource);

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

код:

procedure le_FreeAllLightSources;
var t, tNext: PLightSource;
begin
t:= le_Lights;
while t <> nil do begin
tNext:= t.Next;
le_FreeLightSource(t);
t:= tNext;
end;
end;

Заставим это работать

С типом света мы управились, теперь надо заставить его работать. Объявим следующие переменные:

код:

var // Движок
   le_AlphaBuffer: zglPRenderTarget; // буфер для света
   le_AccBuffer: zglPRenderTarget; // буфер для сложения
   // изображений света
   le_DarkColor: integer; // цвет тени

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

  • zgl_textures – создание текстуры-буфера
  • zgl_render_target – управление рендерингом
  • zgl_primitives_2d – очищение рендер target. Хотя можно было бы просто рисовать QUAD на OpenGL.
  • zgl_fx – процедуры управления блендингом
  • zgl_sprite_2d – рисование текстур

Напишем процедуру инициализации буферов:

код:

procedure le_InitLightEngine(DarkColor:Integer);
begin
   le_DarkColor:=DarkColor;
   // инициализация буферов
   le_AlphaBuffer:=rtarget_Add( RT_TYPE_FBO, tex_CreateZero(
     800,600, 0, TEX_DEFAULT_2D ) , RT_FULL_SCREEN );
   le_AccBuffer :=rtarget_Add( RT_TYPE_FBO, tex_CreateZero(
     800,600, 0, TEX_DEFAULT_2D ) , RT_FULL_SCREEN );
end;

le_DarkColor – переменная, которая отвечает за освещенность. К ее смыслу и принципу работы я еще вернусь. Обобщающая процедура полной обработки света:

код:

procedure le_RenderLight(t:PLightSource);
begin
   rtarget_Set( le_AlphaBuffer ); // начинаем рендер в буфер
   pr2d_Rect(0,0,800,600, 0,255,PR2D_FILL); // очищаем черным
   // цветом
   le_DrawLightSource(t); // отрисовка источника света
   rtarget_Set( nil );
   rtarget_Set( le_AccBuffer ); // отрисовка полученного
   // изобр-я в буфер аккамуляции
   fx_SetBlendMode(FX_BLEND_ADD); // при отрисовке используем
   // блендинг для сложения
   // интенсивностей источ-в света
   ssprite2d_Draw( le_AlphaBuffer.Surface, 0,0,800,600,0);
   fx_SetBlendMode(FX_BLEND_NORMAL);
   rtarget_Set( nil );
end;

И завершающая процедура – вывод буфера с использованием MULT блендинга:

код:

procedure le_FinishRender;
begin
   // выводим полученные тени и свет на экран с использованием
   // блендинга mult (чем светлее изображение тем прозрачней)
   fx_SetBlendMode(FX_BLEND_MULT);
   ssprite2d_Draw( le_AccBuffer.Surface, 0,0,800,600,0);
   fx_SetBlendMode(FX_BLEND_NORMAL);
   // очищаем буфер для следующего кадра (цветом le_DarkColor!)
   rtarget_Set( le_AccBuffer );
   pr2d_Rect(0,0,800,600, le_DarkColor,255,PR2D_FILL);
   rtarget_Set( nil );
end;

Хочу заметить, что очищение le_AccBuffer проводится цветом le_DarkColor. Следовательно, чем светлее этот цвет тем прозрачнее тени (см. рис.10). На изображении видно, что справа фон просвечивается даже там, где света нет, так как le_DarkColor не черный, а серый ($1f1f1f).

рисунок 10

Рис. 10. Различный DarkColor

Все, система света написана. Правда, пока без теней от объектов. Теперь проверим ее в действии.
Добавим переменную под управляемый свет:

код:

var
   UserLight:PLightSource; // указатель на управляемый свет

Создадим его, и еще 4 света в Init:

код:

   le_InitLightEngine(0); // инициализируем световой движок
   // загружаем источники света
   le_CreateLightSource
   (le_v(520,550),400,0.3,IntToRGB($FF00FF));
   le_CreateLightSource
   (le_v(470,50),250,0.5,IntToRGB($FF0000));
   le_CreateLightSource
   (le_v(200,300),270,1,IntToRGB($00FF00));
   le_CreateLightSource
   (le_v(640,270),300,0.8,IntToRGB($FFFFFF));
   UserLight:=le_CreateLightSource(le_v(0 ,
   0),100,0.8,IntToRGB($FFFFFF));

Теперь в Draw напишем рисование теней:

код:

   // обработка всех источников света
   le_EachLightSource(@le_RenderLight);
   // отрисовка теней
   le_FinishRender;

В Update привяжем UserLight к мышке, сделаем изменение размера при нажатии на кнопки мыши и цвета при нажатии на колесико:

код:

   if mouse_Down( M_BLEFT ) then
   UserLight.radius:=UserLight.radius+3;
   if mouse_Down( M_BRIGHT ) then
   if UserLight.radius>0 then
   UserLight.radius:=UserLight.radius-3;
   if mouse_Click( M_BMIDLE ) then begin
   UserLight.color.r:=random(100)/100;
   UserLight.color.g:=random(100)/100;
   UserLight.color.b:=random(100)/100;
end;

И наконец, в Quit очищаем источники света:

код:

   le_EachLightSource(@le_FreeLightSource);

Запускаем, любуемся результатом (см. рис.11).

рисунок 11

Рис. 11. Результат

Заключение

В следующей части статьи я рассмотрю создание теней от объектов, а также оптимизирую код, чтобы добиться большей производительности. Весь исходный код проекта приложен к журналу «ПРОграммист. Пятый выпуск».

Продолжение следует…

Ресурсы

  • Страница разработчика ZenGL http://andrukun.inf.ua/zengl.html
  • Михалкович С.С. Основы программирования.
  • Второй семестр 08-09, 2 часть

Обсудить на форуме – Делаем динамические тени на OPENGL. Часть 1

Похожие статьи