- PVSM.RU - https://www.pvsm.ru -
Как известно, OpenGL версии 1 позволяет использовать не менее восьми штатных источников света. Используя эти источники можно достаточно просто строить качественное цветное освещение без применения карт освещённости. Как оказалось, при разработке самодельных простейших трёхмерных движков, их авторам эта возможность не кажется очевидной и потому ими игнорируется. Описанный ниже способ не подходит для перемещающихся в пространстве источников света, но отлично подходит, если остальные параметры источника света изменяются во времени (например, изменяется цвет источника освещения, или он мигает). Как построить качественное освещение с использованием штатных источников освещения OpenGL и будет рассказано в статье.
Должен сказать, что статья эта планировалась очень давно – года так с 2004, когда был написан трёхмерный движок, использующий описываемый метод. Однако, обилие формул и лень заставили меня уже в скором времени забросить написание статьи. Последний раз я касался текста в 2008 году. На этой дате статья и остановилась, и возрождать я её не планировал. Но недавно, в теме про исходники Quake 2 [1], меня попросили всё же рассказать о применённой в моём трёхмерном движке технологии построения картинки. Актуальность столь запоздавших статей для современного программирования трёхмерной графики, конечно, нулевая, но возможно, начинающим изучать OpenGL эта статья будет интересна.
Для начала я покажу, какие именно получаются изображения при применении описываемого метода. Вот скриншоты моего движка 2004 года:
Скриншоты трёхмерного движка.
Как видите, для простейшего движка получаются достаточно красивые картинки.
Идея алгоритма заключается в том, что многоугольник, источник тени (в дальнейшем – МИТ), своей тенью разбивает многоугольник, приёмник тени (в дальнейшем – МПТ) на фрагменты A, B, C, D, E, как показано на рисунке ниже. Можно заметить, что для фрагментов A, B, E, D источник света включён, а для фрагмента C источник света отключён
Разбиение на фрагменты тенью.
Общий алгоритм построения освещения таков:
В результате такого разбиения мы получим для каждого исходного многоугольника набор фрагментов, для каждого из которых известно, какие источники света его освещают. На практике это означает, что при выводе этих фрагментов движок должен включать или отключать источники света, для которых точно известно, что они данный фрагмент не освещают.
Поскольку мы используем только восемь источников света OpenGL, то при разбиении сцены можно просто выбрать только те источники из общего списка источников света, которые максимально сказываются на выбранном многоугольнике сцены. В самом простом случае это можно сделать, например, посчитав расстояние от источника до вершин многоугольника и выбрать источники с минимальными расстояниями до вершин. Конечно, в этом случае часть источников могут оказаться бесполезными для выбранного многоугольника (источник может вообще быть полностью загорожен другим многоугольником), но зато алгоритм выбора очень прост.
Как видите, метод несложный, однако, потребует алгоритма построения фрагментов, зная МИТ, МПТ и положение источника света.
Условимся считать все многоугольники выпуклыми. Есть это не так, их следует разбить на выпуклые части. Для реализации алгоритма нахождения фрагментов нам понадобятся некоторые функции, а именно:
Функция, определяющая позицию точки относительно некоторой плоскости.
Входные данные:
— Точка плоскости $inline$P(P_{x},P_{y},P_{z})$inline$;
— Вектор нормали к плоскости $inline$n(n_{x},n_{y},n_{z})$inline$;
— Точка, для которой определяется положение относительно плоскости $inline$V(V_{x},V_{y},V_{z})$inline$.
Возвращаемое значение: 1 — точка находится выше плоскости, -1 — точка находится ниже плоскости, 0- точка находится в плоскости.
Для реализации этой функции воспользуемся уравнением плоскости, тогда $inline$ begin{equation}t=n_{x}(V_{x}-P_{x})+n_{y}(V_{y}-P_{y})+n_{z}(V_{z}-P_{z})end{equation} $inline$ (1).
Полученное $inline$t$inline$ определяет положение точки $inline$V$inline$ относительно плоскости.
Функция должна возвращать -1, если $inline$t<0$inline$, возвращать 1, если $inline$t>0$inline$ и возвращать 0, если $inline$t=0$inline$.
Однако, мы работаем на разрядной сетке ЭВМ, поэтому сравнивать с нулём нельзя. Вместо этого мы будем здесь и далее сравнивать с некоторым малым $inline$varepsilon$inline$, значение которого проще всего подобрать опытным путём так, чтобы алгоритм не давал сбоев.
В этом случае функция должна возвращать -1, если $inline$t<-varepsilon$inline$, возвращать 1, если $inline$t>varepsilon$inline$ и возвращать 0 во всех остальных случаях.
Функция, определяющая точку пересечения прямой с плоскостью.
Входные данные:
— Точка плоскости $inline$P(P_{x},P_{y},P_{z})$inline$;
— Вектор нормали к плоскости $inline$n(n_{x},n_{y},n_{z})$inline$;
— Первая точка прямой $inline$A(A_{x},A_{y},A_{z})$inline$;
— Вторая точка прямой $inline$B(B_{x},B_{y},B_{z})$inline$;
Возвращаемое значение: координаты точки пересечения либо флаг, показывающий что пересечения нет.
Вектор $inline$L(L_{x},L_{y},L_{z})$inline$, задающий направление прямой вычисляется как $inline$ begin{array}{c}L_{x}=B_{x}-A_{x},\L_{y}=B_{y}-A_{y},\L_{z}=B_{z}-A_{z}.end{array} $inline$
Тогда скалярное произведение между вектором нормали и вектором прямой будет $inline$E=L_{x}cdot n_{x}+L_{y}cdot n_{y}+L_{z}cdot n_{z}$inline$.
Если $inline$E=0$inline$ (в нашем случае это условие примет вид $inline$Egeq-varepsilon$inline$ и $inline$Eleqvarepsilon$inline$ ), то прямая и плоскость пересекаться не могут, поскольку вектора перпендикулярны.
Если же пересечение возможно, то координаты точки пересечения могут быть получены по формулам
$$display$$begin{array}{c}V_{x}=A_{x}-L_{x}frac{t}{E}\V_{y}=A_{y}-L_{y}frac{t}{E},\V_{z}=A_{z}-L_{z}frac{t}{E}.end{array} $$display$$
где $inline$t$inline$ — значение, полученное по формуле (1).
Функция, определяющая точку пересечения луча с плоскостью.
Входные данные:
— Точка плоскости $inline$P(P_{x},P_{y},P_{z})$inline$;
— Вектор нормали к плоскости $inline$n(n_{x},n_{y},n_{z})$inline$;
— Первая точка луча (начало луча) $inline$A(A_{x},A_{y},A_{z})$inline$;
— Вторая точка луча $inline$B(B_{x},B_{y},B_{z})$inline$.
Возвращаемое значение: координаты точки пересечения либо флаг, показывающий что пересечения нет.
Задача сводится к предыдущей, но появляются дополнительные условия возможности пересечения, поскольку луч, в отличие от прямой, полубесконечен.
Так, если получится, что $inline$-t<E$inline$ для $inline$E>0$inline$ или $inline$t>E$inline$ для $inline$E<0$inline$ (в нашем случае $inline$-t<E+varepsilon$inline$ для $inline$E>0$inline$ или $inline$t>E-varepsilon$inline$ для $inline$E<0$inline$ ), то луч плоскость не пересекает.
Функция, определяющая, нахождение точки внутри выпуклого многоугольника, лежащего в плоскости.
Входные данные:
— Точки многоугольника, лежащие в одной плоскости $inline$P0(P0_{x},P0_{y},P0_{z})...Pn(Pn_{x},Pn_{y},Pn_{z})$inline$;
— Точка, находящаяся в плоскости многоугольника, для которой проверяется её нахождение внутри многоугольника $inline$V(V_{x},V_{y},V_{z})$inline$.
Возвращаемое значение: флаг, показывающий находится точка внутри многоугольника или нет.
Алгоритм работы функции выглядит так:
Введём переменную $inline$a$inline$=индекс последней точки многоугольника;
Цикл по $inline$i$inline$ от 0 до $inline$a$inline$;
Введём переменные индексов трёх последовательных точек $inline$i0=i$inline$,$inline$i1=i+1$inline$,$inline$i2=i+2$inline$;
Если $inline$i1>a$inline$, то $inline$i1=i1-a-1$inline$;
Если $inline$i2>a$inline$, то $inline$i2=i2-a-1$inline$;
Возьмём три последовательные точки многоугольника $inline$A=P(i0)$inline$,$inline$B=P(i1)$inline$,$inline$C=P(i2)$inline$;
Создадим три вектора: $inline$F(F_{x},F_{y},F_{z}),G(G_{x},G_{y},G_{z}),H(H_{x},H_{y},H_{z})$inline$ по формулам
$$display$$ begin{array}{c}F_{x}=B_{x}-A_{x},\F_{y}=B_{y}-A_{y},\F_{z}=B_{z}-A_{z}.end{array}$$display$$
$$display$$ begin{array}{c}G_{x}=C_{x}-A_{x},\G_{y}=C_{y}-A_{y},\G_{z}=C_{z}-A_{z}.end{array}$$display$$
$$display$$ begin{array}{c}H_{x}=V_{x}-A_{x},\H_{y}=V_{y}-A_{y},\H_{z}=V_{z}-A_{z}.end{array}$$display$$
Вычислим векторные произведения $inline$N1(N1_{x},N1_{y},N1_{z})=Ftimes G$inline$ и $inline$N2(N2_{x},N2_{y},N2_{z})=Ftimes H$inline$;
Вычислим скалярное произведение $inline$R=N1_{x}*N2_{x}+N1_{y}*N2_{y}+N1_{z}*N2_{z}$inline$;
Если $inline$R<0$inline$ (в нашем случае $inline$R<-varepsilon$inline$), то точка внутрь многоугольника не попадает.
Конец цикла;
Точка попадает внутрь многоугольника.
Идея работы функции заключается в том, что три подряд идущие точки многоугольника задают два ребра многоугольника, преобразовав которые в вектора, можно вычислить векторное произведение векторов первого ребра со вторым и первого ребра с вектором, составленным из первой точки многоугольника и исследуемой точкой.
При этом, направления получившихся векторов зависят от того, по какую сторону от первого ребра находится точка. Для вычисления согласованности получившихся векторов используется их скалярное произведение, которое будет отрицательным, если вектора направлены в противоположные стороны.
Входные данные:
— Точки $inline$PS(PS0_{x},PS0_{y},PS0_{z})...PSn(PSn_{x},PSn_{y},PSn_{z})$inline$ выпуклого многоугольника, являющегося источником тени), лежащие в одной плоскости;
— Точки $inline$PD(PD0_{x},PD0_{y},PD0_{z})...PDn(PDn_{x},PDn_{y},PDn_{z})$inline$ выпуклого многоугольника, на который отбрасывается тень), лежащие в одной плоскости;
-Точка положения источника света $inline$L(L_{x},L_{y},L_{z})$inline$ (далее по тексту ИС — «источник света»);
Возвращаемые данные: флаг, показывающий существует тень или нет, и массив многоугольников, на которые разбивается МПТ тенью.
Рассмотрим алгоритм получения точек тени в системе координат МПТ.
$$display$$begin{array}{l}V0_{x}=PS0_{x}+N0_{x};\V0_{y}=PS0_{y}+N0_{y};\V0_{z}=PS0_{z}+N0_{z};\V1_{x}=PS0_{x};\V1_{y}=PS0_{y};\V1_{z}=PS0_{z};\V2_{x}=PS1_{x};\V2_{y}=PS1_{y};\V2_{z}=PS1_{z};\V3_{x}=PS1_{x}+N1_{x};\V3_{y}=PS1_{y}+N1_{y};\V3_{z}=PS1_{z}+N1_{z},end{array}$$display$$
где
$$display$$begin{array}{l}N0_{x}=PS0_{x}-L_{x};\N0_{y}=PS0_{y}-L_{y};\N0_{z}=PS0_{z}-L_{z};\N1_{x}=PS1_{x}-L_{x};\N1_{y}=PS1_{y}-L_{y};\N1_{z}=PS1_{z}-L_{z}.end{array}$$display$$
Поскольку МИТ мог быть ориентирован как угодно, то нормали граней теневого объёма могут быть направлены как внутрь объёма, так и наружу. Условимся, что нормали теневого объёма должны быть направлены наружу.
Для этого сравним положение точки, точно попадающей в теневой объём относительно плоскостей, задаваемых гранями теневого объёма. Такая точка может быть получена, если найти геометрический центр МИТ
$$display$$begin{array}{l}x_{c}=frac{sum_{i=0}^{max}PSi_{x}}{max};\y_{c}=frac{sum_{i=0}^{max}PSi_{y}}{max};\z_{c}=frac{sum_{i=0}^{max}PSi_{z}}{max}.end{array}$$display$$
и пустить луч от ИС через этот центр, так же, как это делалось для нахождения вершин граней теневого объёма. Если при определении положения этой точки относительно плоскостей граней будет получено для какой-либо грани, что точка находится выше выбранной грани, то грань следует “перевернуть”, т.е. поменять знаки координат вектора нормали грани на противоположные. Направление обхода самих точек, задающих грань можно не менять, так как это в дальнейшем не понадобится.
Создание фрагмента
Вот, собственно, и весь алгоритм. Все пункты с 1 по 10 можно посмотреть в файле shadow.cpp, находящегося в папке с демонстрационной программой Lighting.
В архиве по ссылке [2] (опять не на github — я с ним пока ещё не подружился) присутствуют программы с исходниками:
Автор: da-nie
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/255461
Ссылки в тексте:
[1] исходники Quake 2: https://habrahabr.ru/post/328128/
[2] ссылке: https://yadi.sk/d/9HgK51XX3JDa4g
[3] Источник: https://habrahabr.ru/post/328842/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.