Реализация карт теней с использованием GLSL шейдеров
Автор: Dmitriy Bespalov
У многих разработчиков возникают трудности при построении теней. Данная статья призвана помочь лучше понять OpenGL реализацию теневых карт на шейдерах.
Многие статьи про карты теней на OpenGL уже устарели, и интерес представляют только для любителей программировать на «антиквариате». Данная статья попытается восполнить этот небольшой пробел.
Начало
Инициализация
Инициализация для варианта #1
Инициализация для варианта #2
Рендер в текстуру
Шейдер записи глубины в текстуру
Рендер сцены с тенями
Матрица источника света
Шейдер сцены с тенью
Режим сравнения текстуры
Заключение
Начало
Принцип построения теней достаточно прост. Всё, что видно с позиции источника света, то освещено, остальное же находится в тени.
Принцип работы можно разделить на два этапа:
1. Сохранение в текстуру глубины сцены с позиции источника света. Такую текстуру называют shadow map.
2. Рендер сцены с тенями, с использованием данных, полученных на первом этапе.
В примере статьи эти два этапа вынесены в отдельные функции RenderToShadowMap() и RenderShadowedScene().
В примере будет использовано два варианта рендеринга теней, отличающихся реализацией. Отличие вариантов заключается в том, что в первом случае мы глубину будем сохранять в текстуру с помощью шейдера и при наложении тени на сцену делать сравнение с текстурой вручную. Во втором случае рендер производится сразу в текстуру глубины без участия шейдеров в этом процессе, а для сравнения используется режим сравнения текстуры, что освобождает нас от ручного сравнения в шейдере.
Инициализация
Инициализация шейдеров и модели тривиальна, вы можете в них разобраться самостоятельно, поэтому описывать её не буду. А вот инициализацию текстур стоит описать подробнее.
Начнём с рассмотрения обоих вариантов рендеринга теней:
Вариант #1: Прикрепить текстуру к FBO как буфер цвета. Для теста глубины используем рендер-буфер.
В этом случае нам придётся производить запись глубины с помощью шейдера. Плюс заключается в том, что мы можем записывать глубину так, как нам захочется. Можем сохранять что-то ещё, помимо глубины, в остальные компоненты текстуры, если она у нас не однокомпонентная.
Вариант #2: Прикрепить текстуру к FBO как буфер глубины. Буфер цвета не используется.
Здесь нам уже не нужен шейдер, чтобы сохранить глубину. Просто рендерим сцену.
Оба варианта имеют как свои плюсы, так и минусы.
Инициализация для варианта #1
Начнём с инициализации для варианта #1. Создадим текстуру, которая будет прикреплена как буфер цвета:
glGenTextures(1, &texDepth); glBindTexture( GL_TEXTURE_2D, texDepth); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); if ( glewIsSupported( "GL_ARB_texture_rg")) { glTexImage2D( GL_TEXTURE_2D, 0, GL_R16, texDepthSizeX, texDepthSizeY, 0, GL_RED, GL_UNSIGNED_SHORT, 0); } else { glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA16, texDepthSizeX, texDepthSizeY, 0, GL_RGBA, GL_UNSIGNED_SHORT, 0); } glBindTexture( GL_TEXTURE_2D, 0);
Я думаю, некоторых пояснений требует используемый формат текстуры.
Начнём с битности. Чем ниже битность, тем ниже точность. Поэтому, при малых битах, больше внимания стоит уделить плоскостям отсечения, нужно ближнюю и дальнюю плоскости расположить как можно ближе к сцене, особенно ближнюю. Иначе могут появится неприятные артефакты из-за нехватки точности. В данном примере я буду использовать 16 бит. Хотя успехов можно добиться и с 8 битной текстурой.
Для хранения глубины нам нужна однокомпонентная текстура. Такие форматы текстур нам даёт поддержка расширения GL_ARB_texture_rg. Если поддержка есть, то используем GL_R16. Можно так же использовать аналог с плавающей запятой GL_R16F. Если же поддержки такого расширения нет, то будем использовать GL_RGBA16. Некоторые видеокарты старого поколения, например, такие как GeForce 6, не поддерживают 16 битных целочисленных текстур, поэтому драйвер будет использовать 8 битные, что может привести к артефактам из-за нехватки точности. Вы можете поэкспериментировать с разными форматами, и выбрать наиболее подходящий.
Предпоследний аргумент type, у функции glTexImage2D(), не имеет в данном случае никакого значения, и типы расставлены для красоты.
Теперь создадим рендер-буфер, он нам нужен для теста глубины: