High Dynamic Range Rendering в OpenGL

Сейчас чуть ли не каждая серьезная игра в число своих возможностей обязательно включает HDR (High Dynamic Range), ну а для демо это уже просто давно стало must have.

Что же на самом деле стоит за термином High Dynamic Range и как использовать этот эффект в своих программах ?

Под Dynamic Range обычно понимают диапазон представляемых (или воспринимаемых) значений. Так для монитора Dynamic Range это соотношение интенсивностей наиболее яркого и наиболее темного представимых цветов.

К сожалению этот параметр для как минимум на порядок уступает как Dynamic Range для реальных сцен (вплоть до 100,000:1), так и возможностям человеческого глаза.

Очень высокий Dynamic Range человеческого глаза определяется способностью глаза по адаптированию к среднему уровню яркости сцены - простое изменение диаметра хрусталика глаза сильно изменяет падающее на сетчатку глаза количество света.

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

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

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

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

Также к таким эффектам обычно относят и так называемый bloom - когда с очень яркой области свет как бы "засвечивает" или закрывает объекты перед ярко освещенными объектами.

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

Если мы хотим по-настоящему поддерживать/моделировать HDR, то мы должны отказаться от приведения всех цветов в единичный RGB-куб - [0,1]3 и крайне желательно осуществлять рендеринг в форматы с плавающей точкой (FP16 или FP32), поскольку эти форматы позволяют хранить значения с очень высоким High Dynamic Range (если мы не можем хранить значения с высоким Dynamic Range, то как же мы будем их показывать и обрабатывать) - стандартный формат TrueColor с 8-битами под каждую цветовую компоненту не обеспечивает достаточно высокого Dynamic Range.

О необходимости использования форматов с плавающей точкой довольно интересно написал Джон Кармак еще в 2000 году.

Отказ от приведения всех цветов в единичный куб [0,1]3 и использование форматов с плавающей точкой позволяет и получать и хранить как сколь угодно темные, так и сколь угодно яркие цвета с очень высокой точностью.

При этом яркие цвет будут сохранять свой настоящий цвет - так приведение в единичный куб цвета (1,1,10) просто переведет его в белый цвет (1,1,1), в то время как в действительности это очень яркий оттенок синего цвета.

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

EN.WIKIPEDIA.ORG/WIKI/IMAGE:HL2HDRCOMPARISON.JPG

Рис 1. Кадры из HalfLife 2: Lost Coast с использованием HDR и без его использования.

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

Поэтому большинство реализаций HDR-рендеринга на современных GPU используют следующий подход - вся сцена (и при необходимости и также отдельные ее части) выводится в floating-point-текстуру (FP16 или FP32).

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

Стандартным способом обработки HDR-изображений является моделирование bloom'а, корректные отражения/преломления (с неприведенными цветами) и tone mapping, осуществляющий приведение цветов с вычетом общей яркости (т.е. не простое отсечение, приводящее к потере цветов, а некоторый аналог работы глаза).

Рассмотрим эти шаги подробнее.

Моделирование эффекта bloom

Для моделирования bloom'а по исходному floating-point-изображению строится новое, меньшего размера в котором происходит фильтрация исходного изображения (чтобы не появился алиасинг) и замена на черный всех цветов, яркость которых ниже заданной.

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

uniform sampler2D mainMap;

void main(void)
{
    vec2 dx   = vec2 ( 1.0 / 512.0, 0.0 );
    vec2 dy   = vec2 ( 0.0, 1.0 / 512.0 );
    vec4 base = texture2D ( mainMap, gl_TexCoord [0].xy ) +
                texture2D ( mainMap, gl_TexCoord [0].xy + dx ) +
                texture2D ( mainMap, gl_TexCoord [0].xy + dy ) +
                texture2D ( mainMap, gl_TexCoord [0].xy + dx + dy );

    base *= 0.25;

    if ( length ( base.rgb ) < 0.8 )
        base.rgb = vec3 ( 0.0 );

    gl_FragColor = base;
}

Следующим шагом является применение к этой текстуре размытия, обычно размытия по Гауссу (Gaussian blur).

В силу свойств размытия по Гауссу его можно реализовать в два прохода - сперва мы "размываем" изображение только по горизонтали, а затем - только по вертикали. В результате мы получаем большой выигрышь в количестве обращений к текстуре (так как количество обращений к текстуре при одностороннем размытии равно размера ядра размытия, а при двухстороннем - квадрату размера ядра, и для размытия с размером ядра 10 и более мы получаем весьма существенный выигрышь).

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

uniform	sampler2D	mainTex;

void main (void)
{
    vec2 tx  = gl_TexCoord [0].xy;
    vec2 dx  = vec2 (0.001953,0.000000);
    vec2 sdx = dx;
    vec4 sum = texture2D ( mainTex, tx ) * 0.134598;

    sum += (texture2D ( mainTex, tx + sdx ) + texture2D ( mainTex, tx - sdx ) )* 0.127325;
    sdx += dx;
    sum += (texture2D ( mainTex, tx + sdx ) + texture2D ( mainTex, tx - sdx ) )* 0.107778;
    sdx += dx;
    sum += (texture2D ( mainTex, tx + sdx ) + texture2D ( mainTex, tx - sdx ) )* 0.081638;
    sdx += dx;
    sum += (texture2D ( mainTex, tx + sdx ) + texture2D ( mainTex, tx - sdx ) )* 0.055335;
    sdx += dx;
    sum += (texture2D ( mainTex, tx + sdx ) + texture2D ( mainTex, tx - sdx ) )* 0.033562;
    sdx += dx;
    sum += (texture2D ( mainTex, tx + sdx ) + texture2D ( mainTex, tx - sdx ) )* 0.018216;
    sdx += dx;
    sum += (texture2D ( mainTex, tx + sdx ) + texture2D ( mainTex, tx - sdx ) )* 0.008847;
    sdx += dx;

    gl_FragColor = sum;
}

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

Tone mapping

Самым простым способом моделирования аккомодации глаза к яркости света является использование параметра exposure, далее обозначаемого через T.

При этом каждая из цветовых компонент подвергается следующему преобразованию:

v'=1-exp(-T*v)

Подобное преобразование легко реализуется при помощи фрагментных шейдеров и гарантированно переводит любое неотрицательное значение в отрезок [0,1].

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

Далее по этому среднему цвету можно найти значение для параметра T.

Другим распространенным вариантом является задание некоторой базовой яркости a. Тогда по исходному изображению находится среднее значение логарифма яркости (обратите внимание на использование небольшой величины, добавляемой к яркости L(x,y), для избегания проблем с поведением логарифма в нуле).

tone mapping equation

После того как по приведенной формуле найдено среднее значение логарифма яркости, все изображение подвергается линейному масштабированию, задаваемому формулой:

tone mapping equation

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

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

tone mapping equation

Можно совместить оба подхода в один -

tone mapping equation

Здесь через Lwhite обозначена минимальная яркость переводимая в чистый белый цвет.

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

tone mapping equation

Необходимые расширения

Для успешной реализации HDR-рендеринга необходима поддержка floating-point-текстур, включая возможность рендеринга в такую текстуру, а также возможность отключения приведения цветов в единичный куб [0,1]3.

На сайте gamedev.ru есть очень хорошая статья по этим вопросам, однако она ориентирована на использование Р-буферов, в то время как гораздо быстрее и проще использовать FBO.

Ниже приводятся краткие описание расширений, которые мы будем в дальнейшем использовать.

ARB_texture_float, ATI_texture_float

Эти расширения (далее мы будем рассматривать только ARB-расширение поскольку они фактически отличаются только именами констант) вводят новые внутренние форматы текстур, соответствующие использованию 16-битовых и 32-битовых floating point чисел для хранения компонент.

При этом для 16-битовых числе принята следующая структура - 1 знаковый бит, 5 под под экспоненту и 10 бит под мантиссу. Формат для 32-битовых числе полностью совпадает с IEEE стандартом для 32-битовых float'ов.

Расширение вводит следующих набор форматов - GL_RGBA32F_ARB, GL_RGB32F_ARB, GL_ALPHA32F_ARB, GL_INTENSITY32F_ARB, GL_LUMINANCE32F_ARB, GL_LUMINANCE_ALPHA32F_ARB, GL_RGBA16F_ARB, GL_RGB16F_ARB, GL_ALPHA16F_ARB, GL_INTENSITY32F_ARB, GL_LUMINANCE16F_ARB и GL_LUMINANCE_ALPHA16F_ARB,

Обратите внимание, что значения отдельных компонент для этих текстур могут выходить за пределы отрезка [0,1].

Кроме введения новых форматов текстур была также добавлена возможность получения информации о хранении компонент внутри текстуры.

Так узнать как именно хранится красная компонента текущей (bound) текстуры можно при помощи следующего фрагмента кода:

GLint type;

glGetTexLevelParameteriv ( GL_TEXTURE_2D, 0, GL_TEXTURE_RED_TYPE_ARB, &type );

В качестве возвращаемых значений могут выступать GL_NONE (такой компоненты в текстуре нет), GL_UNSIGNED_NORMALIZED (хранится как целое нормированное значение) и GL_FLOAT (хранится как floating point число).

Текстуры данных форматов также могут использоваться в FBO в качестве color attachment'а, т.е. в них можно осуществлять рендеринг.

ARB_color_buffer_float

Данное расширение позволяет управлять приведением цветов в единичный куб [0,1]3. При этом управление приведением цвета для вершин отделено от управления приведением цвета для фрагментов.

Данное расширение задает поведение по умолчанию для фрагментов - приведение цветов в единичный куб осуществляется тогда и только тогда, когда осуществляется рендеринг в floating-point текстуру (или буфер). Таким образом при рендеринге в floating point FBO никакого приведения цветов для фрагментов не происходит.

В вершинах цвета по умолчанию всегда приводятся в единичный куб.

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

glClampColorARB ( GLenum targte, GLenum clamp );

В качестве параметра target могут выступать следующие значения - GL_CLAMP_VERTEX_COLOR_ARB, GL_CLAMP_FRAGMENT_COLOR_ARB и GL_CLAMP_READ_COLOR_ARB.

Первые два значения управляют приведением цветов при работе с вершинами и фрагментами. Последний отвечает за приведение цветов при выполнении команды glReadPixels.

Параметр clamp задает требуемый режим и принимает одно из следующих значений GL_FIXED_ONLY_ARB (только при рендеринге в не floating-point буфер), GL_TRUE и GL_FALSE.

ARB_half_float_pixel

Это расширение позволяет загружать данные в текстуру (и читать данные из текстуры) непосредственно в формате FP16.

Это позволят в ряде случаев избежать преобразования данных между типом FP16 и другими типами (например, float).

Ограничения при работе с floating-point текстурами для различных GPU

К сожалению на данный момент практически все GPU, поддерживающие работу с floating-point текстурами накладывают ряд ограничений на работу с ними.

Так для большинства (???) GPU форматов не поддерживается пирамидальное фильтрование (mipmapping), alpha blending.

Так билинейная, трилинейная и анизотропная фильтрация поддерживается только для 16-битовых floating-point текстур, причем начиная с карт GeForce6xxx.

Для серии GeForceFX 5xxx не поддерживаются трехмерные и cubemap floating-point текстуры, фильтрация и alpha blending.

Для серии GeForce 6xxx/7xxx для 32-битовых floating-point текстур поддерживаются трехмерные и cubemap текстуры, , поддержка NPOT-текстур.

Для 16-битовых floating-point текстур также поддерживается би/трилинейная и анизотропная фильтрация, пирамидальное фильтрование, alpha blending.

Форматы файлов для хранения HDR-текстур

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

На данный момент существует два распространенных форматов файлов, позволяющих хранить HDR-текстуры - .hdr и .exr.

Первый их этих форматов появился достаточно давно - он был использован в системе построение реалистический изображений Radiance. Иногда этот формат также называют RGBE (Red, Green, Blue, Exponent).

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

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

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

В прилагаемом коде содержится исходный текст простого загрузчика .hdr-файлов, причем доступны два варианта загрузки текстур:

unsigned loadRgbeAsFloat ( Data * data, unsigned target, unsigned format, bool mipmap = false );
unsigned loadRgbeAsRgba  ( Data * data, unsigned target, unsigned format, bool mipmap = false );

При этом первая функция (loadRgbeAsFloat) честно строит floating-point-текстуру, а вторая строит обычную RGBA-текстуру, только в качестве значений для всех каналов берутся RGBE значения. Таким образом в этом случае задача получения нормального цвета по RGBE-компонентам ложится на шейдер. Преимуществами последнего подхода является гораздо меньшее количество памяти для хранения текстуры и возможность прямо на ходу корректировать экспоненту для текстуры (т.е. фактически управлять ее яркостью).

Другой формат - .exr позволяет хранить все компоненты как 16- или 32-битовые floating point величины, что обеспечивает гораздо более высокую точность задания текстур (но за счет заметно большего размера файла).

Для работы с текстурами в формате .exr проще всего использовать библиотеку OpenEXR, разработанную компанией Industrial Light and Magic.

Правда к сожалению она не компилируется VC 6.

Простейшая реализация HDR-рендеринга

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

После этого произведем рендеринг всей сцены в floating-point FBO, причем обычно вполне достаточно использовать 16-битовые float'ы (одним из недостатков использования floating-point текстур является заметно больших объем памяти что отрицательно сказывается на быстродействии).

Далее по этому FBO построим вспомогательную текстуру вдвое меньшего размера (по каждому измерению), при помощи следующего шейдера:

uniform sampler2D mainMap;

void main(void)
{
    vec2 dx   = vec2 ( 1.0 / 512.0, 0.0 );
    vec2 dy   = vec2 ( 0.0, 1.0 / 512.0 );
    vec4 base = texture2D ( mainMap, gl_TexCoord [0].xy ) +
                texture2D ( mainMap, gl_TexCoord [0].xy + dx ) +
                texture2D ( mainMap, gl_TexCoord [0].xy + dy ) +
                texture2D ( mainMap, gl_TexCoord [0].xy + dx + dy );

    base *= 0.25;

    if ( length ( base.rgb ) < 0.8 )
        base.rgb = vec3 ( 0.0 );

    gl_FragColor = base;
}

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

Далее мы применяем двухпроходное гауссовское размытие (Gaussian blur). Пр размытии мы можем одновременно еще раз уменьшить размер текстуры, но поскольку теперь у нас на каждом проходе идет фильтрацию только в одном направлении, то и уменьшать можно только одно из измерений (соответствующее направлению размытия).

В результате всех этих шагов мы получаем небольшую текстуру с "размытыми" яркими областями.

На заключительном шаге мы прибавляем "размытую" текстуру к исходной и применяем к их сумме tone mapping, реализованный в виде следующего фрагментного шейдера:

uniform sampler2D mainMap;
uniform sampler2D blurMap;
uniform float     exposure;

void main(void)
{
    vec4 base = texture2D ( mainMap, gl_TexCoord [0].xy );
    vec4 blur = texture2D ( blurMap, gl_TexCoord [0].xy );
    vec4 color = base + 5.0 * blur;

    gl_FragColor.rgb = vec3 ( 1.0 ) - exp ( -exposure * color.rgb );
    gl_FragColor.a   = 1.0;
}

Ниже приводится фрагмент кода на С++, отвечающий за "размытие" и наложение текстур:

void blurMap ()
{
    startOrtho ();

    buffer0.bind ();
    filterMap.bind ();
    drawQuad ( buffer.getColorBuffer () );
    filterMap.unbind ();
    buffer0.unbind ();

    buffer2.bind ();
    xBlur  .bind ();                                            // perform x-blurring first
    drawQuad ( buffer0.getColorBuffer () );
    xBlur.unbind   ();
    buffer2.unbind ();

    buffer3.bind ();
    yBlur.bind ();
    drawQuad ( buffer2.getColorBuffer () );
    yBlur.unbind ();
    buffer3.unbind ();
                                                                 // now in buffer3 we have blurred FP map
    endOrtho ();
}

void display ()
{
                                                                  // render for FP FBO
    buffer.bind ();

    reshape ( buffer.getWidth (), buffer.getHeight () );

    glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

                                                                  // draw the light
    glMatrixMode ( GL_MODELVIEW );
    glPushMatrix ();
    glClampColorARB    ( GL_CLAMP_VERTEX_COLOR_ARB, GL_FALSE );   // disable vertex color clamping
    glDisable          ( GL_TEXTURE_2D );
    glTranslatef       ( light.x, light.y, light.z );
    glColor4f          ( 30, 30, 30, 1 );
    glutSolidSphere    ( 0.1f, 10, 10 );
    glPopMatrix        ();

    glMatrixMode   ( GL_MODELVIEW );
    glPushMatrix   ();

    glRotatef    ( rot.x, 1, 0, 0 );
    glRotatef    ( rot.y, 0, 1, 0 );
    glRotatef    ( rot.z, 0, 0, 1 );

    glBindTexture ( GL_TEXTURE_2D, diffMap );

    blinnProgram.bind ();

    drawBox ( Vector3D ( -1, -1, -2 ), Vector3D ( 1.7, 1.7, 1.7 ), decalMap );

    glTranslatef ( 2.5, 2.5, 1 );
    glRotatef    ( 15*angle, 0, 1, 0 );

    glutSolidTorus ( 0.7, 1.6, 20, 20 );

    glRotatef    ( 10*angle, 0, 0, 1 );

    drawBox ( Vector3D ( -2, -2, 1 ), Vector3D ( 1.5, 1.5, 1.5 ), diffMap );

    glEnable ( GL_TEXTURE_2D );

    blinnProgram.unbind ();
    buffer.unbind       ();

    glPopMatrix ();
	
    blurMap ();

    startOrtho ();

    glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    glActiveTextureARB ( GL_TEXTURE1_ARB );
    glBindTexture      ( GL_TEXTURE_2D, buffer3.getColorBuffer () );

    toneMapProgram.bind  ();
    drawQuad ( buffer.getColorBuffer () );
    toneMapProgram.unbind ();

    endOrtho ();

    glutSwapBuffers ();
}

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

HDR image

HDR image

HDR image

К сожалению работа с floating-point текстурами имеет для многих GPU серьезные ограничения, поэтому в некоторых случаях можно смоделировать некоторое подобие HDR и с использованием обычных текстур.

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

Ведь единственным местом, где происходит приведение цветов фрагментов к единичному RGB-кубу - это запись в текстуру, а все расчеты выполняются фрагментным шейдером с использованием floating-point чисел. Однако при сохранении результатов рендеринга в текстуру/фреймбуфер происходит приведение и понижение точности представления (обычно под компоненты отводится всего 8 бит).

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

Так можно исходный цвет вывести неизмененным в один фреймбуфер (и он будет отсечен по [0,1]), а во второй фреймбуфер вывести цвет деленный на 8. В результате у нас как бы получится два изображения, соответствующие разным значениям выдержки. Фактически значения из второго буфера будут содержать значения цвета усеченные до [0,8] (правда с меньшей точностью).

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

Понятно, что это даст некоторое приближение к HDR, однако здесь требуются специальные меры для борьбы с погрешностями - дело в том, что слишком малые значения при представлении в виде байта, просто станут нулями. И наше первоначальное деление на 8 только усугубляет это проблему.

В качестве средства борьбы с этим можно вместо одного умножения на 8 в самом конце, умножать, например, на 2 при каждом размытии и наложении. Это способно несколько сократить превращения темных цветов в черные (поскольку умножать их после того как они станут черным цветом будет уже бессмысленно).

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

Реализация более других вариантов tone mapping

Ниже приводятся фрагментные шейдеры для рассматривающихся вариантов tone mapping'а. Обратите внимание, что для получение среднего значения по всей текстуре достаточно просто прочесть из нее, задав большое смещения для mipmap-уровня (если пирамидальное фильтрование поддерживается и пирамида построена, то ее вершина как раз и будет содержать среднее значения для всей текстуры).

//
// Simple Reinhard tone mapping (log/exp)
//

uniform sampler2D mainMap;
uniform sampler2D blurMap;

uniform float grayLevel;

void main(void)
{
    vec4  base   = texture2D ( mainMap, gl_TexCoord [0].xy );
    vec4  blur   = texture2D ( blurMap, gl_TexCoord [0].xy );
    float avgLum = exp ( texture2D ( blurMap, gl_TexCoord [0].xy, 12 ).a );

    base = base * (grayLevel / avgLum);

    vec4 color = base + 5.0 * blur;

    gl_FragColor = vec4 ( color.rgb, 1.0 );
}

// 
// Simple tone mapping using L/(1+L) mapping
//

uniform sampler2D mainMap;
uniform sampler2D blurMap;


void main(void)
{
    const vec3  luminance = vec3 ( 0.3, 0.59, 0.11 );
    vec4        base      = texture2D ( mainMap, gl_TexCoord [0].xy );
    vec4        blur      = texture2D ( blurMap, gl_TexCoord [0].xy );
    float       l         = dot ( luminance, base );
    float       scale     = l / ( 1.0 + l );

    vec4 color = base * scale + 5.0 * blur;

    gl_FragColor = vec4 ( color.rgb, 1.0 );
}

//
// Composite log/exp and L/(1+L)
//

uniform sampler2D mainMap;
uniform sampler2D blurMap;

uniform float lWhite;


void main(void)
{
    vec4  base   = texture2D ( mainMap, gl_TexCoord [0].xy );
    vec4  blur   = texture2D ( blurMap, gl_TexCoord [0].xy );
    float l      = dot ( vec3 ( 0.3, 0.59, 0.11 ), base );
    float scale  = l / ( 1.0 + l ) * (1.0 + l / (lWhite*lWhite));
	
    vec4 color = base * scale + 5.0 * blur;

    gl_FragColor.rgb = color.rgb;
    gl_FragColor.a   = 1.0;
}

//
// Using average of squared L
//

uniform sampler2D mainMap;
uniform sampler2D blurMap;

uniform float grayLevel;

void main(void)
{
    vec4  base   = texture2D ( mainMap, gl_TexCoord [0].xy );
    vec4  blur   = texture2D ( blurMap, gl_TexCoord [0].xy );
    float avgLum = sqrt ( 0.0001 + texture2D ( blurMap, gl_TexCoord [0].xy, 12 ).a );

    base = base * (grayLevel / avgLum);
    base = base / ( vec4 ( 1.0 ) + base );

    vec4 color = base + 5.0 * blur;

    gl_FragColor = vec4 ( color.rgb, 1.0 );
}

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

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