Создание теней при помощи теневых объемов

Одним из наиболее распространенных приемов построения теней в реальном времени является использование так называемых теневых объемов (Shadow Volumes).

Теневой объем - это область пространства, закрываемая от заданного источника света (см. рис. 1) каким-либо объектом (объектами).

example of shadow volume

Рис 1. Пример теневого объема.

На рисунке 1 показана область пространства, закрываемая объектом А от источника света, расположенного в точке L. Фактически каждый объект сцены создает свой теневой объем по отношению к каждому источнику света. Совокупность теневых объемов, создаваемых различными объектами сцены по отношению к заданному источнику света, определяет общий теневой объем для данного источника света.

Любая точка, попадающая в этот теневой объем, находится в тени, а любая точка, лежащая вне его, освещается данным источником света.

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

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

Если считать теневой объем замкнутым телом, то для проверки на попадание точки внутрь него можно использовать обычный тест принадлежности точки многоугольнику (из двухмерной геометрии) - из точки выпускается луч (причем направление этого луча принципиальной роли не играет).

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

Если число пересечений луча с лицевыми гранями равно числу пересечений луча с нелицевыми гранями, то точка лежит вне теневого объема (см рис. 2).

generic ray intersection

Рис 2. Пересечение луча с границей в простейшем случае.

На рисунке 2 знаком "+" обозначены пересечения луча с лицевой гранью, а знаком "-" - пересечения с нелицевой гранью. Как видно для точки А число пересечений с лицевыми гранями (одно) равно числу пересечений с нелицевыми гранями (одно) и поэтому точка А лежит вне теневого объема.

Для точки В имеет место всего одно пересечение (с нелицевой гранью) и она лежит внутри теневого объема.

Этот подход работает и для сложных теневых объемов, являющихся объединением нескольких элементарных теневых объемов (см рис. 3).

checks for complex shadow volume

Рис 3. Проверка на попадание в сложный теневой объем.

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

Однако какой бы сложной не была структура общего теневого объема, описанный алгоритм проверки на попадание точки внутрь теневого объема, по-прежнему работает.

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

Для начала заметим, что поскольку можно выпускать лучи в произвольном направлении, то можно в качестве этого произвольного направления использовать направление на наблюдателя (камеру).

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

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

Рассмотрим следующий простой пример (в двухмерном случае)(см. рис 4.) - отрезок АВ создает теневой объем ABA'B' по отношению к источнику света в точке L и мы хотим определить какая часть отрезка CD попадает в этот теневой объем.

depth test for shadow faces

Рис 4. Прохождение теста глубины теневыми гранями.

Для этого сначала выведем отрезка CD с записью в буфер глубины (без освещения) и посмотрим, как будут проходить тест глубины грани теневого объема AA' и BB' (как видно в данном случае только они играют роль).

Грань AA' полностью проходит тест глубины (и как легко заметить имеет по одному пересечению выпущенного в направлении наблюдателя луча для каждой точки отрезка CD) и является лицевой по отношению к этому наблюдателю.

А вот (нелицевая) грань BB' лишь частично пройдет тест глубины - его часть BF этот тест пройдет, а часть FB' не пройдет.

Легко заметить, что для произвольной точки отрезка DF, луч выпущенный в направлении наблюдателя, будет иметь ровно одно пересечение с гранью BB' (и, тем самым, данная точка будет находиться вне теневого объема).

А вот для точки отрезка CF, луч, выпущенный в направлении наблюдателя не будет иметь ни одного пересечения с гранью BB' (но, при этом, будет иметь одно пересечение с гранью AA'). Таким образом, эти точки оказываются внутри теневого объема.

Тем самым, мы приходим к довольно простому алгоритму - сперва в буфер глубины выводится грани сцены (в нашем случае - грань CD).

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

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

Далее проверяем выполнение теста глубины для всех нелицевых граней теневого объема. При прохождении теста значение в буфере трафарета уменьшаем на единицу.

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

Все остальные точки находятся внутри теневого объема.

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

Обратите внимание, что сперва следует выводить грани, увеличивающие значение в буфере трафарета, а только потом уменьшающие. Это связано с тем, что по стандарту OpenGL отсекает значения из буфера трафарета, поэтому уменьшение нулевого значения с последующим его увеличением может и не дать нуль в итоге (далее мы рассмотрим расширение EXT_stencil_wrap, позволяющее снять это ограничение).

Именно этот подход к построению теней обычно и называется Stencil Shadow Volumes, причем, поскольку изменение значения в буфере трафарета происходит только при прохождении теста глубины, то за этим подходом закрепилось еще одно название - z-pass.

Данный алгоритм можно выразить следующим фрагментом псевдокода.

                        # draw all polygons without lighting
    for p in polys:
        p.drawUnlit ()

                        # now setup for shadow polys
    shadowPolys = computeShadowPolys ()

                        # disable writing to color and depth buffers
    glColorMask ( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE )
    glDepthMask ( GL_FALSE )

                        # setup stencil
    glEnable      ( GL_STENCIL_TEST )
    glStencilFunc ( GL_ALWAYS, 0, ~0 )
    glStencilOp   ( GL_KEEP,            # stencil test fail
                    GL_KEEP,            # depth test fail
                    GL_INCR )           # depth test pass

                        # draw all front-faced shadow polys
    for s in shadowPolys:
        if s.isFrontFacing ( eye ):
             s.draw ();

                        # setup stencil
    glStencilOp   ( GL_KEEP,            # stencil test fail
                    GL_KEEP,            # depth test fail
                    GL_DECR )           # depth test pass

                        # draw all back-faced shadow polys
    for s in shadowPolys:
        if not s.isFrontFacing ( eye ):
             s.draw ();

                       # now we can draw lighted polys with shadowing
    glColorMask   ( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE )
    glDepthFunc   ( GL_EQUAL )
    glStencilFunc ( GL_EQUAL, 0, ~0 ) 

    for p in polys:
        p.drawLighted ()

Метод z-pass можно заметно упростить, если заметить, что для него достаточно выводить только небольшую часть граней.

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

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

Оказывается, что для алгоритма z-pass достаточно выводить только грани, получающиеся вытягиванием силуэтных ребер (см. рис 5.)

protruding silhouettes edges

Рис 5. Вытягивание ребер силуэта.

Тем самым, можно переписать алгоритм z-pass следующим образом:

                        # draw all polygons without lighting
    for p in polys:
        p.drawUnlit ()

                        # now setup for shadow polys
    silhouette  = polys.getSilhouetteEdges ( light )
    shadowPolys = extrudeEdges ( silhouette, light )

                        # disable writing to color and depth buffers
    glColorMask ( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE )
    glDepthMask ( GL_FALSE )

                        # setup stencil
    glEnable      ( GL_STENCIL_TEST )
    glStencilFunc ( GL_ALWAYS, 0, ~0 )
    glStencilOp   ( GL_KEEP,            # stencil test fail
                    GL_KEEP,            # depth test fail
                    GL_INCR )           # depth test pass

                        # draw all front-faced shadow polys
    for s in shadowPolys:
        if s.isFrontFacing ( eye ):
             s.draw ();

                        # setup stencil
    glStencilOp   ( GL_KEEP,            # stencil test fail
                    GL_KEEP,            # depth test fail
                    GL_DECR )           # depth test pass

                        # draw all back-faced shadow polys
    for s in shadowPolys:
        if not s.isFrontFacing ( eye ):
             s.draw ();

                       # now we can draw lighted polys with shadowing
    glColorMask   ( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE )
    glDepthFunc   ( GL_EQUAL )
    glStencilFunc ( GL_EQUAL, 0, ~0 ) 

    for p in polys:
        p.drawLighted ()

Однако у данного подхода существует и весьма серьезный недостаток - отсечение грани теневого объема по ближней плоскости отсечения (near clip plane) может привести к ошибочным результатам.

В ситуации, изображенной на рисунке 6 часть грани AA' оказывается отсеченной по ближней плоскости отсечения, что приводит к тому, что для отсеченной части, даже несмотря на то, что тест глубины был бы пройден, значения в буфере трафарета окажутся неизменными.

example of clipping causing incorrect shadows

Рис 6. Отсечение теневой грани приводит к некорректному вычислению тени.

when camera is inside volume error occurs

Рис 7. При нахождении наблюдателя в тени также происходит неверное вычисление тени.

В результате точка Р ошибочно будет принята за освещенную.

Подобная ситуация также возникает, когда сам наблюдатель находится внутри теневого объема (см. рис. 7). Как видно только грань BB' не будет полностью отсечена по усеченной пирамиде видимости (vewing furstum), поэтому мы ошибочно примем часть CF за освещенную.

Для борьбы с этим недостатком Джон Кармак предложил следующую модификацию алгоритма, получившую название z-fail (именно она и была использована в игре Doom 3):

Как и ранее отдельно выводятся лицевые и нелицевые грани теневого объема, но изменения в буфере трафарет происходят при не прохождении теста глубины (z-fail).

Сперва выводятся все нелицевые грани границы теневого объема и для каждого фрагмента, не прошедшего тест глубины, соответствующее значение в буфере трафарета увеличивается на единицу.

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

Фактически это соответствует выпусканию луча не к наблюдателю, а от него. Рассмотрим это подробнее (см. рис. 8). По отношению к наблюдателю, расположенному в точке Е грань AA' является лицевой, грань BB' - нелицевой.

z-fail approach

Рис 8. Метод z-fail.

Сперва выводится нелицевая грань BB'. В точке P' тест глубины выполняется и значение в буфере трафарета не изменяется. Для точки Q' тест глубины не выполняется и значение в буфере трафарета увеличивается на единицу.

Так как для грани AA' тест глубины всюду выполнен, то ее вывод не изменяет значения в буфере трафарета.

Таким образом, значение в буфере трафарета, соответствующее точке P' равно нулю и эта точка лежит вне теневого объема. Для точки Q значение в буфере трафарета оказывается равным единице, следовательно эта точка находится в тени.

Обратите внимание, что в отличие от z-pass здесь уже недостаточно выводить грани, получающиеся при вытягивании силуэтных ребер. Здесь требуется замкнутый теневой объем, поэтому необходимо также вывести все лицевые грани объекта (по отношению к источнику света), а все нелицевые - спроектировать в бесконечность.

Покажем, что для этой модификации алгоритма отсечение по ближней плоскости не представляет проблемы.

near plane clipping for z-fail

Рис 9. Отсечение по ближней плоскости не опасно для метода z-fail.

Хотя часть грани AA' и оказывается отсеченной по ближней плоскости отсечения, но поскольку для всей этой грани тест глубины выполнен, то он не оказывает никакого влияния на значения в буфере трафарета (что вполне естественно, поскольку мы выпускаем луч в направлении от наблюдателя).

Ниже приводится псевдокод для метода z-fail.

                        # draw all polygons without lighting
    for p in polys:
        p.drawUnlit ()

                        # now setup for shadow polys
    shadowPolys = computeShadowPolys ()

                        # disable writing to color and depth buffers
    glColorMask ( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE )
    glDepthMask ( GL_FALSE )

                        # setup stencil
    glEnable      ( GL_STENCIL_TEST )
    glStencilFunc ( GL_ALLWAYS, 0, ~0 )
    glStencilOp   ( GL_KEEP,            # stencil test fail
                    GL_INCR,            # depth test fail
                    GL_KEEP )           # depth test pass

                        # draw all back-faced shadow polys
    for s in shadowPolys:
        if not s.isFrontFacing ( eye ):
             s.draw ();

                        # setup stencil
    glStencilOp   ( GL_KEEP,            # stencil test fail
                    GL_DECR,            # depth test fail
                    GL_KEEP )           # depth test pass

                        # draw all front-faced shadow polys
    for s in shadowPolys:
        if s.isFrontFacing ( eye ):
             s.draw ();

                       # now we can draw lighted polys with shadowing
    glColorMask   ( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE )
    glDepthFunc   ( GL_EQUAL )
    glStencilFunc ( GL_EQUAL, 0, ~0 ) 

    for p in polys:
        p.drawLighted ()

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

В следующей таблице приводятся сравнение методов z-pass и z-fail.

Операцияz-passz-fail
Увеличивает значение в буфере трафаретаПри прохождении теста глубиныПри не прохождении теста глубины
Увеличивает значение дляЛицевых гранейНелицевых граней
Уменьшает значение дляНелицевых гранейЛицевых граней
Порядок вывода гранейсперва лицевые, потом нелицевыеСперва нелицевые, потом лицевые
Какая плоскость отсечения представляет проблемуБлижняяДальняя

Как видно из приведенной таблицы оба варианта метода (и z-pass и z-fail) подвержены возникновению ошибок из-за влияния плоскостей отсечения. Только для подхода z-pass угрозу представляет отсечение по ближней плоскости отсечения, а для z-fail - дальняя.

В случае отсечения границ теневого объема по дальней плоскости отсечения возникают ошибки, аналогичные методу z-pass.

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

Для этого возможны следующие подходы:

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

Второй способ заключается в использовании расширения NV_depth_clamp (поддерживаемого начиная с GeForce 3). Данное расширение позволяет включить специальный режим (при помощи команды glEnable ( GL_DEPTH_CLAMP_NV )) обработки глубины.

В этом режиме вообще не происходит отсечения по ближней и дальней плоскостям, все грани растеризуются и глубина каждого фрагмента приводится в отрезок [zNear,zFar].

При этом объекты, лежащие за (или пересекающие) дальней плоскостью отсечения растеризуются и получившиеся фрагменты обрабатываются, только их глубина приводится в диапазон [zNear,zFar].

Таким образом все равно выполняется проверка глубины и изменение значений в буфере трафарета.

Третий подход заключается в модификации стандартной матрицы перспективного проектирования задаваемой обычно командой glFrustum. Обычно эта матрица выглядит следующим образом:

projection matrix

При этом только третья строка этой матрицы зависит от zNear и zFar. Обратите внимание, что существует конечный предел при zFar стремящемся к бесконечности.

Переходя к пределу (при zFar стремящемся к бесконечности) получаем следующую матрицу проектирования:

projection matrix for zFar in infinity

Использование этой матрицы в качестве матрицы проектирования приводит к тому, что даже очень удаленные вершины не будут отсекаться. В качестве бесконечно удаленных точек будут выступать точки вида (x,y,z,0) (в однородных координатах).

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

Ниже приводится функция, заменяющая glFrustum и вводящая описанный выше способ проектирования.

void    infFrustum ( float left, float right, float bottom, float top, float zNear, float zFar )
{
    float   proj [16];                          // new projection matrix

    proj [0 ] = 2*zNear/(right - left);         // P (0,0)
    proj [1 ] = 0;                              // P (0,1)
    proj [2 ] = 0;                              // P (0,2)
    proj [3 ] = 0;                              // P (0,3)

    proj [4 ] = 0;                              // P (1,0)
    proj [5 ] = 2*zNear/(top - bottom);         // P (1,1)
    proj [6 ] = 0;                              // P (1,2)
    proj [7 ] = 0;                              // P (1,3)

    proj [8 ] = (right+left)/(right - left);    // P (2,0)
    proj [9 ] = (top+bottom)/(top-bottom);      // P (2,1)
    proj [10] = -1;                             // P (2,2)
    proj [11] = -1;                             // P (2,3)

    proj [12] = 0;                              // P (3,0)
    proj [13] = 0;                              // P (3,1)
    proj [14] = -2*zNear;                       // P (3,2)
    proj [15] = 0;                              // P (3,3)

    glMultMatrixf ( proj );
}

В результате мы приходим к довольно простому методу построения теневых объемов.

Весь теневой объем для заданного источника света L строится как объединение элементарных объемов. Все грани, ограничивающие эти элементарные теневые объемы, разделяются на три класса:

shadow volume boundary types

Рис 10. Типы граней, ограничивающих теневой объем.

Так на рисунке 9, AB соответствует грани первого типа, A'B' соответствует грани второго типа, а грани AA' и BB' - третьего типа.

Рассмотрим подробнее построение всех типов граней при использовании в качестве матрицы проектирования рассмотренной ранее матрицы Pinf.

Пусть ABC - грань объекта, лицевая по отношению к источнику света (т.е. первого типа). Будем считать, что каждая из вершин задается своими однородными координатами:
A=(ax,ay,az,aw),
B=(bx,by,bz,bw),
C=(cx,cy,cz,cw)

Также будем считать, что точечный источник света L задан своими однородными координатами (lx,ly,lz,lw).

Вытягивая вершины грани ABC по направлению от источника света в бесконечность дает нам следующие вершины:
Ainf=(axlw - lxaw,aylw-lyaw,azlw-lzaw,0),
Binf=(bxlw - lxbw,bylw-lybw,bzlw-lzbw,0),
Cinf=(cxlw - lxcw,cylw-lycw,czlw-lzcw,0),

Тогда грань AinfBinfCinf будет гранью второго типа для элементарного теневого объема, порождаемого гранью ABC.

Каждое из ребер AB, BC и AC (если они силуэтные)при вытягивании порождает грань третьего типа - ABBinfAinf, BCCinfBinf и ACCinfAinf.

Таким образом по произвольной грани ABC мы построили все грани, определяющие соответствующий теневой объем.

Повторяя подобную процедуру для каждой грани сцены, лицевой по отношению к источнику света, мы получим набор граней, ограничивающих полный теневой объем для данного источника света и можем свободно применять метод z-fail для получения корректных теней с точностью до пиксела.

Можно сократить число проходов для вывода границ теневого объема, если воспользоваться следующими двумя расширениями - EXT_stencil_wrap и EXT_stencil_two_side (NV_stencil_two_side).

Первое из этих расширений (EXT_stencil_wrap) позволяет при задании операции над буфером трафарета вместо стандартных GL_INCR и GL_DECR использовать GL_INCR_WRAP_EXT и GL_DECR_WRAP_EXT, гарантирующие что порядок, в котором будут выполняться эти операции не будет влиять на конечное значение в буфере трафарета.

Расширение EXT_stencil_two_side (основанное на расширении NV_stencil_two_side) позволяет задавать разные операции над буфером трафарета для лицевых и нелицевых граней.

При этом функция glActiveStencilFaceEXT задает для какого типа граней (GL_FRONT или GL_BACK) сейчас будет задаваться операция над буфером трафарета.

Использование этих двух расширений позволяет выводить все грани теневого объема в один проход, при этом для лицевых граней будет выполняться одна операция, а для нелицевых - другая. Расширение EXT_stencil_wrap гарантирует, что грани могут выводиться в произвольном порядке.

Ниже приводится фрагмент кода, выводящий грани теневого буфера.

                        # disable writing to color and depth buffers
    glColorMask ( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE )
    glDepthMask ( GL_FALSE )
    glDisable   ( GL_CULL_FACE )

                        # setup stencil
    glEnable      ( GL_STENCIL_TEST )
    glEnable      ( GL_STENCIL_TEST_TWO_SIDE_EXT )

                        # setup stencil for back faces
    glActiveStencilFaceEXT ( GL_BACK )
    glStencilMask          ( ~0 )
    glStencilFunc          ( GL_ALWAYS, 0, ~0 )

    glStencilOp   ( GL_KEEP,            # stencil test fail
                    GL_INCR_WRAP_EXT,   # depth test fail
                    GL_KEEP )           # depth test pass

                        # setup stencil for front faces
    glActiveStencilFaceEXT ( GL_FRONT )
    glStencilMask          ( ~0 )
    glStencilFunc          ( GL_ALWAYS, 0, ~0 )

    glStencilOp   ( GL_KEEP,            # stencil test fail
                    GL_DECR_WRAP_EXT,   # depth test fail
                    GL_KEEP )           # depth test pass

                        # draw all shadow polys at once
    for s in shadowPolys:
        s.draw ()

Для корректного использования метода теневых объемов (как z-pass, так и z-fail) требуется, чтобы все объекты, отбрасывающие тени были замкнутыми многообразиями (т.е. любое ребро принадлежало бы ровно двум граням).

Также необходима информация не только о вершинах и гранях, но и обо всех ребрах объекта. Ниже приводятся описания соответствующих структур и метод, осуществляющий построение массива ребер по набору граней.

struct  Vertex
{
    Vector4D    pos;                        // position of vertex
    Vector2D    tex;                        // texture coordinates
    Vector3D    n;                          // unit normal
    Vector3D    t, b;                       // tangent and binormal
    Vector4D    inf;                        // projection into infinity
};

struct  Face                                // triangular face
{
    int         index [3];                  // indices to Vertex array
    Vector3D    n;                          // face normal
    bool        frontFacing;
};

struct  Edge
{
    int     a, b;                           // indices into array of Vertex
    int     f1, f2;                         // indices into array of Face's
    bool    rev1, rev2;                     // whether the order of points was reversed
                                            // for faces f1 and f2
};

void Torus :: buildEdges ()
{
                                            // create edges
    edges    = new Edge [3*numFaces];       // use max size
    numEdges = 0;

    for ( i = 0; i < numFaces; i++ )
    {
        addEdge ( faces [i].index [0], faces [i].index [1], i );
        addEdge ( faces [i].index [1], faces [i].index [2], i );
        addEdge ( faces [i].index [2], faces [i].index [0], i );
    }
}

void    Torus :: addEdge ( int a, int b, int face )
{
    int     a1  = a;
    int     b1  = b;
    bool    rev = false;
    
    if ( a > b )
    {
	a1  = b;
	b1  = a;
	rev = true;
    }

                                            // check whether we already have this face
    for ( int i = 0; i < numEdges; i++ )
        if ( edges [i].a == a1 && edges [i].b == b1 )
        {
            assert ( edges [i].f2 == -1 );

            edges [i].f2   = face;
            edges [i].rev2 = rev;

            return;
        }

                                            // insert new adge
    edges [numEdges].a    = a1;
    edges [numEdges].b    = b1;
    edges [numEdges].f1   = face;
    edges [numEdges].rev1 = rev;
    edges [numEdges].f2   = -1;

    numEdges++;
}

Обратите внимание на следующее обстоятельство - для каждой грани должен быть задан такой порядок вершин, чтобы при включении отсечения нелицевых граней (командой glCullFace ( GL_BACK ) ) мы получали бы корректное изображение объекта.

Ниже приводится исходный код для функции, осуществляющий вывод теневого объема для метода z-fail (полный исходный код можно скачать по ссылке в конце статьи).

void    drawShadowVolume ( Face * faces, int numFaces, Edge * edges, int numEdges, 
                           Vertex * vertices, int numVertices )
{
    int i;
                                            // draw extruded silhouette edges
    glBegin ( GL_QUADS );

    for ( i = 0; i < numEdges; i++ )
    {
        assert ( edges[i].f1 != -1 );
        assert ( edges[i].f2 != -1 );

        if ( faces [edges[i].f1].frontFacing == faces [edges[i].f2].frontFacing )
            continue;
                                            // we've found silhoette edge
                                            // extrude it
                                            // use correct order for AB from front-facing facet
        int ia, ib;

        if ( faces [edges[i].f1].frontFacing )
        {
            if ( edges [i].rev1 )           // whether order of points differs from f1
            {
                ia = edges [i].b;
                ib = edges [i].a;
            }
            else
            {
                ia = edges [i].a;
                ib = edges [i].b;
            }
        }
        else                                // faces [edges [i].f2] is frontFacing
        {
            if ( edges [i].rev2 )
            {
                ia = edges [i].b;
                ib = edges [i].a;
            }
            else
            {
                ia = edges [i].a;
                ib = edges [i].b;
            }
        }

        Vertex& a = vertices [ia];
        Vertex& b = vertices [ib];

                                            // draw extruded AB
        glVertex4fv ( b.pos );
        glVertex4fv ( a.pos );
        glVertex4fv ( a.inf );
        glVertex4fv ( b.inf );
    }

    glEnd   ();

                                            // now draw front-facing the light faces & 
                                            // their projections
    glBegin ( GL_TRIANGLES );

    for ( i = 0; i < numFaces; i++ )
    {
        Vertex& a = vertices [faces [i].index [0]];
        Vertex& b = vertices [faces [i].index [1]];
        Vertex& c = vertices [faces [i].index [2]];


        if ( faces [i].frontFacing )
        {
                                            // draw ABC
            glVertex4fv ( a.pos );
            glVertex4fv ( b.pos );
            glVertex4fv ( c.pos );
        }
        else
        {                                    // draw ABC's projection
            glVertex4fv ( a.inf );
            glVertex4fv ( b.inf );
            glVertex4fv ( c.inf );
        }
    }

    glEnd ();
}

Рассмотрим теперь различные оптимизации для работы с теневыми объемами.

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

Удобнее всего все вершинные данные сразу же переписать в память графического ускорителя ( например, используя VBO), тогда вывод теневого объема будет заключаться просто в передачи набора индексов вершин для вывода.

Еще одним местом, позволяющим получить выигрыш в быстродействии является комбинирование обоих подходов - и z-pass и z-fail.

Дело в том, что метод z-pass заметно эффективнее, чем z-fail (в первую очередь за счет значительно меньшего числа выводимых граней).

Поэтому можно для одних загораживающих объектов использовать z-fail, а для других - z-pass (см. рис. 11).

Рис 11. Совместное использование z-pass и z-fail.

Одним из простейших (и достаточно эффективных) способов проверки, можно ли для данного объекта использовать метод z-pass является следующий - по передней части усеченной пирамиды видимости и положению источника света строится пирамида (см. рис. 12). Если объект не попадает в эту пирамиду (для этой проверку можно использовать ограничивающие тела), то для него можно безопасно использовать метод z-pass.

Рис. 12. В одном случае можно использовать z-pass, а в другом необходимо использовать z-fail.

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

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

В обоих из них строится ограничивающее тела для области влияния источника света.

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

glScissor ( x, y, width, height );
glEnable  ( GL_SCISSOR_TEST );

Второй способ использует расширения EXT_depth_bounds_test. Данное расширения позволяет ввести еще один тест глубины - тест на принадлежность нормированного значения глубины (т.е. приведенного в отрезок [0,1]) заданному отрезку [z1,z2].

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

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

glEnable         ( GL_DEPTH_BOUNDS_TEST_EXT );
glDepthBoundsEXT ( z1, z2 );

Оба эти способа могут использоваться независимо друг от друга и позволяют эффективно снижать fillrate при построении теневого объема.

Рис 13. Использование ограничения на область влияния источника света.

Ниже приводится изображения полученное при использовании метода z-fail. Можно заметить, что на самом торе тень имеет клеточную структуру, т.е. как бы состоит из блоков.

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

shadow volume screenshot

Рис 14. Пример использование теневых объемов.

Кроме расширения NV_stencil_two_side есть его аналог для карт от ATI - ATI_separate_stencil. Данное расширения вводит следующие функции

glStencilOpSeparateATI   ( GLenum face,      GLenum sfail, GLenum dpfail, GLenum dppass );
glStencilFuncSeparateATI ( GLenum frontFunc, GLenum backFunc, GLint ref, GLuint mask );
В этих функциях параметр face задает грань, для которой определяется операция и может принимать одно из следующих значений - GL_FRONT, GL_BACK и GL_FRONT_AND_BACK.

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

Использование данного расширения для ATI-шных карт полностью аналогично использованию расширений NV_stencil_two_side и EXT_stencil_two_side.

По этой ссылке можно скачать весь исходный код к этой статье. Также доступны для скачивания откомпилированные версии для M$ Windows, Linux и Mac OS X.

Valid HTML 4.01 Transitional

Напиши мне
Используются технологии uCoz