Последние записи
- Перенести программу из Delphi в Lazarus
- Определить текущую ОС
- Автоматическая смена языка (раскладки клавиатуры)
- Сравнение языков на массивах. Часть 2
- wprintf как напечатать кириллицу
- Взаимодействие через командную строку
- Сравнение языков на массивах. Часть 1
- Сравнение языков по скорости
- Чтение огромных xml-файлов
- Как в Python+Selenium webdriver открыть новую вкладку в уже открытом браузере?
Интенсив по Python: Работа с API и фреймворками 24-26 ИЮНЯ 2022. Знаете Python, но хотите расширить свои навыки?
Slurm подготовили для вас особенный продукт! Оставить заявку по ссылке - https://slurm.club/3MeqNEk
Online-курс Java с оплатой после трудоустройства. Каждый выпускник получает предложение о работе
И зарплату на 30% выше ожидаемой, подробнее на сайте академии, ссылка - ttps://clck.ru/fCrQw
25th
Авг
Делаем динамические тени на OPENGL. Часть 1
Posted by Chas under Журнал, Статьи
Здравствуйте. В этой статье я хочу рассмотреть создание движка динамического освещения с помощью графической библиотеки OpenGL. Писаться движок будет на Delphi, но это не мешает переписать его на любой другой язык, так как главное, рассматриваемое в статье, это алгоритмы…
Вадим Буренков
vadim_burenkov@mail.ru
Чтобы не тратить время на инициализацию OpenGL и избежать других проблем (например, с настройкой таймеров и рендера в текстуру) я буду использовать движок ZenGL [1]. Впрочем, от него нам многого не понадобится. Итак, приступим…
Инициализация OpenGL в ZenGL
Первым делом качаем ZenGL и создаем в нем простейшее приложение (вы можете найти его в папке LightEngine ресурсов статьи, там же вы найдете ZenGL):
код:
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. Пути
Рис. 2. Тайлинг
Немного теории
Теперь перейдем к теории вопроса. В движке мы должны реализовать два типа – источники света и объекты, которые отбрасывают тени (см. рис.3):
Рис. 3. Тень от объекта
Источник света обладает параметрами:
- положение
- радиус
- цвет
- интенсивность
Все объекты являются невыпуклыми многоугольниками. Они имеют:
- локальные координаты вершин
- мировые координаты вершин
- количество вершин
- положение
- угол поворота
Локальные координаты нужны, так как через положение и угол поворота объекта его можно разворачивать.
Для хранения данных об освещенности нам понадобятся два буфера размером в экран. Первый – альфа буфер. В него выводится круглый источник света (см. рис.4):
Рис. 4. Источник света
После этого альфа буфер рисуется во второй буфер – буфер аккумуляции. При этом используется аддитивный режим блендинга, то есть цвета смешиваются. В буфере мы получаем такую картинку (см. рис.5):
Рис. 5. Смешивание источников света
В нем светлые участки – там где свет, а темные – там где тьма. А теперь мы выводим буфер аккамуляции на экран с блендингом MULT. Получается так, что чем светлее цвет, тем он прозрачнее (см. рис.6):
Рис. 6. Рисование с блендингом MULT
Как же делаются тени от объектов? При выводе источника света в альфа буфер на него рисуется форма тени черным цветом. Получается что от круга света «отрезают» кусок (см. рис.7):
Рис. 7. Форма тени на свете
С помощью такого алгоритма получаются тени любой сложности, причем их количество, как и источников света с объектами неограниченно (см.рис.8).
Да будет свет!
Под следующий код сделаем модуль, который и будет отвечать за тени. Назовем его ZGLShadows.
Рис. 8. Сцена с большим количеством теней
Напишем тип света:
код:
PLightSource=^TLightSource;
TLightSource=record
position:leVect; // положение
radius:single; // радиус
color:TColorRGB; // цвет
intensivity:single; // интенсивность cвета
prev,next:PLightSource;
end;
Все данные будут храниться в “prev-next” (двухсвязных) списках (см. рис.9):
Рис. 9. Списки
Каждый элемент является звеном цепи и имеет указатели на предыдущее и следующее звено. Нам же нужно иметь первый элемент и длину цепи для управления списком:
код:
// Источники света
le_Lights:PLightSource; // список
le_NumLights:integer; // количество
Более подробно о такой системе хранения данных можно почитать в Интернете. Как мы видим, у нас появились новые типы данных:
код:
TColorRGB=record
r,g,b:single;
end;
Данный тип нужен для хранения цвета в формате rgb, так как его использует OpenGL. ZenGL использует integer для хранения цветов (например $FFFFFF соответствует 1,1,1 в RGB), поэтому нам может понадобится процедура для перевода цвета в RGB:
код:
begin
Result.r := ((Color and $FF0000) shr 16) / 255;
Result.g := ((Color and $FF00) shr 8) / 255;
Result.b := (Color and $FF) / 255;
end;
Все координаты будут храниться в типе:
код:
leVect=record
x,y:single;
end;
Подробнее о нем будет написано позже, пока нам понадобится только формирование вектора по x и y:
код:
begin
result.x:=x;
result.y:=y;
end;
Перейдем к процедурам управления источниками света:
код:
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. Первая точка в центре имеет цвет и интенсивность света, далее идут точки по радиусу окружности с нулевым цветом, благодаря чему мы имеем плавный переход цвета:
код:
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;
Количество треугольников, из которого рисуется круг, задается константой:
код:
le_numSubdivisions = 32;
Следующая процедура рассчитывает и рисует тень для объектов, но о ней я напишу позже:
код:
Далее напишем процедуру, которая освобождает память, занятую источником света:
код:
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;
Следующая процедура вспомогательная, она обрабатывает каждый источник света передаваемой в нее процедурой:
код:
var t, tNext: PLightSource;
begin
t:= le_Lights;
while t <> nil do
begin
tNext:= t.Next;
p(t);
t:= tNext;
end;
end;
le_proc – тип процедуры:
код:
le_proc=procedure(d:Pointer);
Например, данная строчка нарисует все источники света:
код:
Хочу заметить, что при попытке передать в le_EachLightSource процедуру очищения возникнет ошибка, связанная с памятью, поэтому для очищения всех источников света напишем отдельную процедуру:
код:
var t, tNext: PLightSource;
begin
t:= le_Lights;
while t <> nil do begin
tNext:= t.Next;
le_FreeLightSource(t);
t:= tNext;
end;
end;
Заставим это работать
С типом света мы управились, теперь надо заставить его работать. Объявим следующие переменные:
код:
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 – рисование текстур
Напишем процедуру инициализации буферов:
код:
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 – переменная, которая отвечает за освещенность. К ее смыслу и принципу работы я еще вернусь. Обобщающая процедура полной обработки света:
код:
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 блендинга:
код:
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. Различный DarkColor
Все, система света написана. Правда, пока без теней от объектов. Теперь проверим ее в действии.
Добавим переменную под управляемый свет:
код:
UserLight:PLightSource; // указатель на управляемый свет
Создадим его, и еще 4 света в Init:
код:
// загружаем источники света
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 к мышке, сделаем изменение размера при нажатии на кнопки мыши и цвета при нажатии на колесико:
код:
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 очищаем источники света:
код:
Запускаем, любуемся результатом (см. рис.11).
Рис. 11. Результат
Заключение
В следующей части статьи я рассмотрю создание теней от объектов, а также оптимизирую код, чтобы добиться большей производительности. Весь исходный код проекта приложен к журналу «ПРОграммист. Пятый выпуск».
Продолжение следует…
Ресурсы
- Страница разработчика ZenGL http://andrukun.inf.ua/zengl.html
- Михалкович С.С. Основы программирования.
Второй семестр 08-09, 2 часть
Обсудить на форуме – Делаем динамические тени на OPENGL. Часть 1
Похожие статьи
Купить рекламу на сайте за 1000 руб
пишите сюда - alarforum@yandex.ru
Да и по любым другим вопросам пишите на почту
пеллетные котлы
Пеллетный котел Emtas
Наши форумы по программированию:
- Форум Web программирование (веб)
- Delphi форумы
- Форумы C (Си)
- Форум .NET Frameworks (точка нет фреймворки)
- Форум Java (джава)
- Форум низкоуровневое программирование
- Форум VBA (вба)
- Форум OpenGL
- Форум DirectX
- Форум CAD проектирование
- Форум по операционным системам
- Форум Software (Софт)
- Форум Hardware (Компьютерное железо)