Бесплатная реклама

Автор: Владимир Дьячков «Nikola Tesla»

Опубликовано: 17.03.2008

Изменено: 17.03.2008

Постоянная ссылка

Комментарии [1]

Реализация процессора эффектов постобработки. Часть 2 - Создание пост-эффектов


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



Страницы: 1 2 3
Содержание

ПРЕДИСЛОВИЕ
Создание пост-эффектов на основе фреймворка
      Простейшие эффекты - сепия, монохром, инверсия цвета
      Техника ping-pong – простейший способ размыть изображение
      Bloom эффекты
      Пост-эффекты с доступом к текстуре глубины: эффект глубины пространства (DOF)
      Пост-эффекты с доступом к текстуре глубины: эффект колебания горячего воздуха
      Sun Shafts
      HDR эффект, аккомодация глаза к интенсивности освещения
ЗАКЛЮЧЕНИЕ
ПРИЛОЖЕНИЕ
      Демо версия + исходный код
      Немного о работе демки
СПИСОК ИСТОЧНИКОВ

 

ПРЕДИСЛОВИЕ

Теперь начинается самое интересное. Пора испытать фреймворк в деле. Для этого попытаемся создать с его помощью несколько популярных пост-эффектов. Начнем с самых простых эффектов, таких как сепия и монохром (обесцвечивание). Все необходимые файлы пост-эффектов располагаются в директории BIN\DATA и имеют расширение .ppf, все шейдеры для постобработки находятся в директории BIN\DATA\PostProc и имеют расширение .glsl. Каждый такой glsl файл содержит код вершинного и фрагментного шейдеров на языке GLSL.

Создание пост-эффектов на основе фреймворка

Простейшие эффекты - сепия, монохром, инверсия цвета

Пост-эффект сепия - это особый способ тонирования изображения, перевод цветного изображения из ярких оттенков в желтовато-коричневые тона. Сепия применяется в основном фотографами для создания ощущения старого фотоснимка. Для получения такого результата сначала рассчитывается интенсивность каждого фрагмента исходного изображения. Для каждого цвета определен его вклад в интенсивность. Для R, G и B компонент цвета значения этого вклада (веса) таковы: 0.27, 0.67 и 0.06 соответственно. Видно, что наибольший вклад в интенсивность освещения вносит зеленый цвет.

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

      Colorrgb = DarkColorrgb * (1.0 - lum) + LightColorrgb * lum,

где lum - интенсивность данного фрагмента, DarkColorrgb - цвет самого темного участка получаемого изображения, а LightColorrgb - цвет самого светлого. DarkColor и LightColor может задаваться художником для получения приемлемого результата. Зададим DarkColorrgb = vec3(0.2,0.05,0.0); а LightColorrgb = vec3(1.0,0.9,0.5);

Ниже представлен листинг вершинного и фрагментного шейдеров для такого типа тонирования.

sepia.glsl

/*VERTEX_SHADER*/
void main(void)
{
	gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
	gl_TexCoord[0] = gl_MultiTexCoord0;
}

/*FRAGMENT_SHADER*/
uniform sampler2D image;
const vec3 LUMINANCE_WEIGHTS = vec3(0.27, 0.67, 0.06);
const vec3 LightColor = vec3(1.0,0.9,0.5);
const vec3 DarkColor = vec3(0.2,0.05,0.0);

void main(void)
{
	vec3 col = texture2D ( image, gl_TexCoord[0].xy ).xyz;

	float lum = dot(LUMINANCE_WEIGHTS,col);
	vec3 sepia = DarkColor*(1.0-lum) + LightColor*lum;
	gl_FragColor = vec4(sepia,1.0);
}

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

Теперь несколько слов о настройке пост-эффекта. Нам понадобится только один рендер-таргет, в котором будет формироваться исходное изображение для обработки. Его разрешение выберем 512х512, а внутренний формат текстуры RGBA8 (нам ведь нужна будет альфа компонента цвета?). Также нам будет нужен буфер глубины, поэтому в секции render_target прописываем depth 24. Текстура нам тоже нужна, иначе, что мы будем обрабатывать? Поэтому пишем texture 0. Значение 0 - означает, что текстура будет привязана на нулевой цветовой буфер. Дополнительные текстуры нам не понадобятся. Вот как будет выглядеть файл пост-эффекта:

mm_sepia.ppf

!----------------------------------
!	Postproc Filter	File
!----------------------------------
! ВНИМАНИЕ: задается имя пост-эффекта, оно должно в точности совпадать 
! с именем файла пост-эффекта
mm_sepia.ppf
{
	! описание используемых рендер-таргетов
	! temp0 - имя рендер-таргета
	render_target  temp0
	{
		! зададим размеры
		width		512
		height		512
		! зададим формат
		int_format	RGBA8
		! нам нужен будет буфер глубины, поэтому пишем:
		Depth		24
		! также нам нужна текстура для исходного изображения
		texture	0
	}

	! описание используемых шейдеров
	! sepia - имя шейдера
	shader  sepia
	{
		! обязательно задается файл с исходным кодом шейдера
		file		PostProc/sepia.glsl
		! Теперь настроим сэмплеры шейдера.
		! Для сэмплера с именем image следует использовать текстуру 
		! на нулевом текстурном модуле
		texture	image	0
		! здесь можно также установить значения uniform-переменных
		! если они имеются в шейдере. В данном случае нам настраивать
		! нечего.
	}
  
	! теперь ВАЖНО! указываем в какой рендер-таргет следует выводить
	! 3D сцену перед ее обработкой. В нашем случае
	! этим рендер-таргетом является temp0
	begin_target	temp0

	! теперь задаем проходы - т.е. процедуры вывода квада
	! во фреймбуфер. p0 - имя прохода.
	pass	p0
	{
		! зададим текстуру для обработки в шейдере
		! первым после слова texture идет имя сэмплера в шейдере
		! вторым - либо имя рендер-таргета с текстурой, либо
		! имя текстуры описываемой в секции input_texture. В нашем
		! случае необходимо обработать изображение сформированное 
		! в текстуре, которая присоединена к рендер-таргету temp0. 
		! Следующим параметром идет номер аттачмента рендер-таргета
		! с которого нужно взять текстуру,
		! у нас один аттачмент - нулевой, поэтому его и указываем
		texture	image		temp0		0
		! Затем указываем рендер-таргет, в который будет 
		! осуществляться вывод обработанного в данном проходе 
		! изображения. У нас несложный эффект требующий всего 
		! одного прохода, поэтому выводим обработанное изображение 
		! сразу на экран, т.е. в задний буфер backbuffer.
		! ВНИМАНИЕ! слово backbuffer - зарезервировано, поэтому если
		! назвать один из рендер-таргетов именем backbuffer, то
		! последствия могут быть непредсказуемыми
		target	backbuffer
		! теперь укажем имя шейдера который будет осуществлять
		! обработку
		shader	sepia
	}

	! последовательности. Все возможности, которые может дать эта 
	! секция нам не понадобятся, у нас простой эффект с одним проходом,
	! поэтому просто создаем одну секцию и указываем в ней 
	! единственный проход
	! seq0 - имя секции
	sequence seq0
	{
		! задаем наш единственный проход p0
		pass	p0
	}
}

На рисунке 1 показан результат применения пост-эффекта mm_sepia.ppf.

Рис. 1. Результат постобработки фильтром Сепия
Рис. 1. Результат постобработки фильтром Сепия

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

mono.glsl

/*FRAGMENT_SHADER*/
uniform sampler2D image;
const vec3 LUMINANCE_WEIGHTS = vec3(0.27, 0.67, 0.06);

void main(void)
{
	vec3 col = texture2D (image, gl_TexCoord[0].xy).xyz;

	float lum = dot(LUMINANCE_WEIGHTS,col);
	gl_FragColor = vec4(lum,lum,lum,1.0);
}

На рисунке 2 показан результат применения пост-эффекта mm_mono.ppf.

Рис. 2. Результат постобработки фильтром Desaturate или монохром
Рис. 2. Результат постобработки фильтром Desaturate или монохром.

Инверсия цвета такой же простейший эффект, поэтому я только лишь упомяну о его существовании. Думаю с ним и так все ясно.

Техника ping-pong - простейший способ размыть изображение

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

Для этого нам понадобится два рендер-таргета. Один будет размера 128х128, другой должен быть в 2, 4, или 8 раз меньше. Еще будет нужен простейший шейдер, который просто копирует текстуру на экран.

PingPong.glsl

/*FRAGMENT_SHADER*/
uniform sampler2D BlurSampler;

void main(void)
{
	vec4 color = texture2D(BlurSampler, gl_TexCoord[0].xy);
	gl_FragColor = color;
}

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

mm_pingPongBlur.ppf

!----------------------------------
!	Postproc Filter	File
!----------------------------------
mm_pingPongBlur.ppf
{
   ! описание используемых рендер-таргетов
   render_target  temp0
   {
	width		128
	height		128
	int_format	RGBA8
	depth		24
	texture		0
   }

   render_target  temp1
   {
	width		64
	height		64
	int_format	RGB8
	texture		0
   }

   ! описание используемых шейдеров
   shader  blur
   {
	file		PostProc/PingPong.glsl
	texture		BlurSampler	0
   }  

   begin_target	temp0

   pass	ping
   {
	texture	BlurSampler	temp0	0
	target	temp1
	shader	blur
   }
   pass	pong
   {
	texture	BlurSampler	temp1	0
	target	temp0
	shader	blur
   }
   pass	final
   {
	texture	BlurSampler	temp0	0
	target	backbuffer
	shader	blur
   }

   sequence	seq1
   {
	pass	ping
	pass	pong
	pass	ping
	pass	pong
	pass	ping
	pass	pong
	pass	ping
	pass	pong
	pass	ping
	pass	pong
	pass	ping
	pass	pong
	pass	ping
	pass	pong
	pass	ping
	pass	pong
	pass	ping
	pass	pong
	pass	final
   }
}

Имеем три прохода, первый ping - рендеринг квада с текстурой рендер-таргета temp0 в рендер-таргет temp1, второй - pong - рендеринг квада с текстурой рендер-таргета temp1 в рендер-таргет temp0. Третий проход - final - рендеринг квада с текстурой рендер-таргета temp0 в задний буфер. Последовательность seq1 задает используемые проходы, попеременно используются проходы ping и pong несколько раз до получения желаемого уровня размытия, в конце выполняется проход final для вывода результата на экран.

Рис. 3. Результат размытия изображения методом пинг-понг
Рис. 3. Результат размытия изображения методом пинг-понг

Bloom эффекты

Bloom эффект - эффект когда свет от наиболее ярких участков изображения накладывается на более темные. Bloom эффект осуществляется в несколько этапов. Во-первых, исходное изображение уменьшается и все фрагменты изображения проходят так называемый bright pass, когда цвет фрагментов с маленькой яркостью ужимается до 0. Таким образом, остаются яркими только те фрагменты, яркость которых больше определенной величины, остальные фрагменты имеют цвет vec3(0.0,0.0,0.0). Затем это изображение размывается, например пинг-понгом, и смешивается с исходным.

Нужно 3 рендер-таргета, один с буфером глубины для исходного изображения и два рендер-таргета для блура пинг-понгом. Также понадобится 3 шейдера, один bright pass шейдер с уменьшением (down sampling), один для пинг-понга и один для финального смешивания. Для bright-pass шейдера нужно передать массив uniform переменных для осуществления down sampling-a. Передача переменных осуществляется заданием var_vec2_array в секции shader, значения должны следовать через символ табуляции или пробел, перевод строки не допускается.

downSampleLDR.glsl

/*FRAGMENT_SHADER*/
uniform sampler2D DownSampler;
uniform vec2 samples[16];
vec4 SuppressLDR( vec4 c )
{
	if ( c.r > 1.0 || c.g > 1.0 || c.b > 1.0 )
		return c;
	else
		return vec4( 0.0, 0.0, 0.0, 0.0 );
}

void main(void)
{
	vec3 col;
	vec2 sample;
	for (int i = 0; i < 16; i++)
	{
		sample = vec2(samples[i].x,samples[i].y);
		col += texture2D(DownSampler, gl_TexCoord[0].xy + sample).xyz;
	}
	col *= 0.0625;
	
	gl_FragColor = SuppressLDR( vec4(col*1.1, 1.0) );
}

Во фрагментном шейдере усредняются цвета 16-ти текселей исходной текстуры, смещение для выборки задается массивом uniform переменных samples[16]. Затем над полученным цветом производится bright pass, подавляющий все фрагменты, значение цвета какой-либо компоненты которых меньше единицы. Так как исходное изображение находится в низком динамическом диапазоне и все компоненты <= 1.0, то чтобы увеличить яркость и выделить только яркие участки, перед bright pass цвета умножаются на коэффициент 1.1.

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

bloom.glsl

/*FRAGMENT_SHADER*/
uniform sampler2D FullSampler;
uniform sampler2D BlurSampler;
uniform float	bloomBlendFactor;
void main(void)
{
	vec4 col = texture2D(FullSampler, gl_TexCoord[0].xy);
	vec4 bloom = texture2D(BlurSampler, gl_TexCoord[0].xy);

	col += bloom * bloomBlendFactor;
	col.a = 1.0;

	gl_FragColor = col;
}

Ниже приведен листинг файла пост-эффекта mm_bloom.ppf.

mm_bloom.ppf

!----------------------------------
!	Postproc Filter	File
!----------------------------------
mm_bloom.ppf
{
   ! описание используемых рендер-таргетов
   render_target  source
   {
	width		512
	height		512
	int_format	RGBA8
	depth		24
	texture		0
   }
   render_target  brightPass
   {
	Width		128
	height		128
	int_format	RGBA8
	texture		0
   }
   render_target  Blur
   {
	Width		64
	height		64
	int_format	RGBA8
	texture		0
   }

   ! описание используемых шейдеров
   shader  downsample
   {
	file		PostProc/downSampleLDR.glsl
	texture	DownSampler	0
	var_vec2_array	samples 16 -0.0029296875 -0.0029296875 -0.0009765625 
	-0.0029296875  0.0009765625 -0.0029296875 0.0029296875 -0.0029296875 
	-0.0029296875 -0.0009765625 -0.0009765625 -0.0009765625 0.0009765625 
	-0.0009765625 0.0029296875 -0.0009765625 -0.0029296875 0.0009765625 
	-0.0009765625 0.0009765625 0.0009765625 0.0009765625 0.0029296875 
	0.0009765625 -0.0029296875 0.0029296875 -0.0009765625 0.0029296875 
	0.0009765625 0.0029296875 0.0029296875 0.0029296875 
   }
	
   shader  blur
   {
	file		PostProc/pingpong.glsl
	texture		BlurSampler	0
   }  
   shader  bloom
   {
	file		PostProc/bloom.glsl
	texture		FullSampler		0
	texture		BlurSampler		1
	var_float	bloomBlendFactor	2.0
   }

   ! Начальный рендер таргет
   begin_target		source


   ! Используемые проходы
   pass	p0
   {
	texture	DownSampler	source	0
	target	brightPass
	shader	downsample
   }
   pass	p1
   {
	texture	BlurSampler	brightPass	0
	target	Blur
	shader	blur
   }
   pass	p2
   {
	texture	BlurSampler	Blur	0
	target	brightPass
	shader	blur
   }
   pass	p3
   {
	texture	FullSampler	source		0
	texture	BlurSampler	brightPass	0
	target	backbuffer
	shader	bloom
   }

   ! Используемые последовательности
   sequence	seq0
   {
	pass	p0
	pass	p1
	pass	p2
	pass	p1
	pass	p2
	pass	p1
	pass	p2
	pass	p1
	pass	p2
	pass	p1
	pass	p2
	pass	p1
	pass	p2
	pass	p1
	pass	p2
	pass	p3
   }
}
Рис. 4. Результат применения Bloom эффекта
Рис. 4. Результат применения Bloom эффекта

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

Ниже приведен шейдер для размазывания изображения по заданному направлению, dx - вектор этого направления.

xBlurStar.glsl

/*FRAGMENT_SHADER*/
uniform sampler2D BlurXSampler;
uniform float blurness;
void main(void)
{
    vec2 tx  = gl_TexCoord [0].xy;
    vec2 dx  = vec2 (0.01953,0.01953)*blurness;
    vec2 sdx = dx;
    vec4 sum = texture2D (BlurXSampler, tx) * 0.134598;

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

    gl_FragColor = sum;
}

Ниже приведен шейдер для финального смешивания. В xBlur и yBlur - передаются текстуры с заблуренными по разным направлениям изображениями, в FullSampler находится исходное изображение. Как и в предыдущем случае, интенсивность блума контролируется переменной bloomBlendFactor.

bloom_star.glsl

/*FRAGMENT_SHADER*/
uniform sampler2D FullSampler;
uniform sampler2D yBlur;
uniform sampler2D xBlur;
uniform float bloomBlendFactor;
void main(void)
{
	vec4 col = texture2D(FullSampler, gl_TexCoord[0].xy);
	vec4 bloomy = texture2D(yBlur, gl_TexCoord[0].xy);
	vec4 bloomx = texture2D(xBlur, gl_TexCoord[0].xy);

	col += (bloomy+bloomx) * bloomBlendFactor;
	col.a = 1.0;

	gl_FragColor = col;
}

mm_bloomstar.ppf

!----------------------------------
!	Postproc Filter	File
!----------------------------------
mm_bloomstar.ppf
{
   ! описание используемых рендер-таргетов
   render_target  source
   {
	width		512
	height		512
	int_format	RGBA8
	depth		24
	texture		0
   }
   render_target  brightPass
   {
	width		256
	height		256
	int_format	RGBA8
	texture		0
   }
   render_target  BlurX
   {
	width		256
	height		256
	int_format	RGBA8
	texture		0
   }
   render_target  BlurY
   {
	width		256
	height		256
	int_format	RGBA8
	texture		0
   }

   ! описание используемых шейдеров
   shader  downsample
   {
	file		PostProc/downSampleLDR.glsl
	texture		DownSampler	0
	var_vec2_array	samples	16	-0.0029296875	-0.0029296875	-0.0009765625 	
	-0.0029296875	 0.0009765625	-0.0029296875	 0.0029296875	-0.0029296875	
	-0.0029296875 	-0.0009765625	-0.0009765625	-0.0009765625	0.0009765625	
	-0.0009765625	0.0029296875	-0.0009765625	-0.0029296875	0.0009765625	
	-0.0009765625	0.0009765625	0.0009765625	0.0009765625	0.0029296875	
	0.0009765625	-0.0029296875	0.0029296875	-0.0009765625	0.0029296875	
	0.0009765625	0.0029296875	0.0029296875	0.0029296875
   }
   shader  blurx
   {
	file		PostProc/xBlurStar.glsl
	texture		BlurXSampler	0
	var_float	blurness	0.325
   }
   shader  blury
   {
	file		PostProc/yBlurStar.glsl
	texture		BlurYSampler	0
	var_float	blurness	0.325
   }  
   shader  bloom
   {
	file		PostProc/bloom_star.glsl
	texture		FullSampler	0
	texture		yBlur		1
	texture		xBlur		2
	var_float	bloomBlendFactor	0.8
   }

   ! Начальный рендер таргет
   begin_target		source


   ! Используемые проходы
   pass	p0
   {
	texture	DownSampler	source	0
	target	brightPass
	shader	downsample
   }
   pass	p1
   {
	texture	BlurXSampler	brightPass	0
	target	BlurX
	shader	blurx
   }
   pass	p2
   {
	texture	BlurYSampler	brightPass	0
	target	BlurY
	shader	blury
   }
   pass	p3
   {
	texture	FullSampler	source	0
	texture	yBlur		BlurY	0
	texture	xBlur		BlurX	0
	target	backbuffer
	shader	bloom
   }

   ! Используемые последовательности
   sequence	seq0
   {
	pass	p0
	pass	p1
	pass	p2
	pass	p3
   }
}

Имеем несколько проходов. p0 осуществляет bright pass и одновременно down sampling исходного изображения. p1 смазывает изображение прошедшее bright pass по одному направлению, p2 - по другому. p3 - финальный проход с выводом результата на экран.

Рис. 5. Результат применения Bloom Star эффекта
Рис. 5. Результат применения Bloom Star эффекта

 

Страницы: 1 2 3