Пишем GLSL шейдеры в RenderMonkey - Specular Lighting + Heat Haze
Здравствуйте всем!
Сегодня мы рассмотрим с Вами в одном уроке сразу два эффекта, широко применяемых при создании современных игр – это попиксельное бликовое освещение (specular lighting) и эффект искажения сцены как от горячего воздуха (или heat haze). И реализовывать все это мы будем в такой замечательной среде как ATI Render Monkey. При написании статьи использовалась версия 1.6.
Итак приступим. Начнем с того что создадим новый эффект. Для этого кликаем правой кнопкой мыши на Effect Workspace и в меню Add Effect Group выбираем пункт Effect Group w/ OpenGL Effect. Теперь переименуем наш эффект, например OpenGL_HeatHaze. Сделать это можно выделив эффект, и в его контекстном меню выбрать пункт Rename, или просто нажать F2. Теперь Вы должны видеть примерно такую картину.
Теперь раскроем наш эффект. Вы видите что он включает в себя StreamMapping, Model и Pass 0. StreamMapping это параметры определяющие атрибуты вершины, по умолчанию вершина содержит следующие атрибуты: позиция ( position ) [ vec3 ], нормаль к вершине ( normal ) [ vec3 ] и текстурные координаты ( texcoord ) [ vec2 ]. Вы имеете право изменять эти данные, добавлять новые ( например касательные вектора – тангент и бинормаль ), менять и размерность и т.д. Model – как ясно из названия, это трехмерная модель, использующаяся для рендеринга. И наконец Pass 0 – это проход рендеринга, коих может быть несколько. В свою очередь проход может содержать в себе ссылки на параметры атрибутов ( StreamMapping ), модели, тектстуры, камеры и др.
В первом проходе мы будем отрисовывать модель используя бликовую модель освещения Блинна соответствуя следующей формуле:
color = Ia * Ca + Id * Cd + Is * Cs; |
Где I – интенсивность, C – цвет, a – фоновая ( ambient ) составляющая, d – рассеянная ( diffuse ) составляющая, s – бликовая ( specular ) составляющая освещения;
Фоновая составляюшая у нас будет константной. Диффузная будет задаваться текстурой и освещением данной точки, которое вычисляется формулой
D = max( dot( L, N ), 0.0 ) * texture; |
где L – единичный вектор направления на источник света, N – нормаль в данной точке. В нашем примере мы будем брать нормаль в вершине и интерполировать ее вдоль всего примитива. Функция max будет предохранять нас от появления «отрицательной» освещенности, когда dot( L, N ) < 0;
Бликовую составляющую мы будем вычислять по следующей формуле:
S = ( max( dot( N, H ), 0.0 ) ^ P ) * C; |
где N – нормаль в данной точке, H – полувектор направления на источник света и на камеру, который при условии единичности векторов находится по формуле
H = normalize( L + V ); |
где L – единичный вектор направления на источник света, V – единичный вектор направления на камеру.
P – показатель степени в которую возводится скалярное произведение; чем он больше – тем ярче и резче блик.
И наконец C – цвет блика.
Фух-х-х, с теорией закончили, перейдем к практике. Для начала поменяем модель шарика на модель скажем слоника =). Для этого найдем в нашем эффекте ( не в проходе! ) кликнем правой кнопкой мыши на пункте Model и в меню Change Model найдем ElephantBody.3ds; Теперь перед нами симпатишный слоник =). Насколько я помню, слоны в жизни не блестят, но зато блестит паркет, поэтому давайте покроем нашего слоника новеньким паркетом. Для этого добавим к нашему эффекту текстуру. Для этого кликнем на эффекте правой кнопкой мыши и выберем пункт Add Texture -> Add 2D Texture -> Wood.dds . Засада подумали Вы, слоник не изменился! И действительно, для того чтобы слоник стал паркетным этого мало, нужно добавить в проход Текстурный Объект ( Texture Object ) а также немного изменить шейдеры.
Для этого кликнем на нашем проходе правой кнопкой мыши и выберем пункт Add Texture Object. Назовем его например diffuseMap, раскроем его, кликнем правой кнопкой мыши на строке baseMap и выберем пункт Reference Node -> Wood. Текстурный объект создан. Зададим теперь все необходимые параметры для шейдеров. Параметры могут быть как стандартными ( определены самим Render Monkey, Predefined ), например положение камеры, прошедшее время и т.д., так и задаваемыми самим пользователем, например цвет, коэффициенты и т.д. Давайте добавим стандартный параметр, определяющий положение камеры. Для этого кликнем на эффекте правой кнопкой мыши и выберем пункт Add Variable -> Float -> Predefined -> vViewPosition. Это мы положение нашей камеры. Теперь зададим свои параметры. Зададим параметр отвечающий за вклад фонового освещения. Для этого кликнем правой кнопкой мыши на эффекте и выберем Add Variable -> Float -> Float. Это создаст параметр типа Float. Давайте назовем его ambientI, кликнем два раза и зададим значение 0.2. Теперь зададим позицию источника света, выберем Add Variable -> Float -> Float3, назовем его lightPosition зададим ему значения 0.0, 100.0, 100.0. Теперь добавте самостоятельно еще такие параметры:
specPower – Float = 32.0 ( сила блика )
ambientColor и specColor – Color ( цвет фонового освещения и бликов ), значения на свое усмотрения. Я установил для фонового светло-серый цвет, а для бликов чистый белый.
С параметрами мы разобрались, осталось запрограммировать шейдеры. Кликнем 2 раза на строке Vertex Program и наберем такой вот текст шейдера:
uniform vec3 lightPosition; uniform vec4 vViewPosition; varying vec3 normal; varying vec3 lightDir; varying vec3 vh; varying vec2 tc0; void main(void) { vec3 pos = gl_Vertex.xyz; lightDir = normalize( lightPosition - pos ); vec3 eyeDir = normalize( vViewPosition.xyz - pos ); vh = normalize( lightDir + eyeDir ); normal = gl_NormalMatrix * gl_Normal; gl_Position = ftransform(); tc0 = gl_MultiTexCoord0.xy; } |
а для Fragment Program:
uniform float ambientI; uniform vec4 ambientColor; uniform float specPower; uniform vec4 specColor; uniform sampler2D diffuseMap; varying vec3 normal; varying vec3 lightDir; varying vec3 vh; varying vec2 tc0; void main(void) { vec3 n = normalize( normal ); vec4 ambient = ambientI * ambientColor; vec4 diffuse = max( dot( normalize( lightDir ), n ), 0.0 ) * texture2D( diffuseMap, tc0 ); vec4 specular = pow( max( dot( n, normalize( vh ) ), 0.0 ), specPower ) * specColor; gl_FragColor = ambient + diffuse + specular; } |
Теперь сохраните все выбрав File->Save ( CTRL+S ) и откомпилируйте шейдеры нажав
В результате наших шаманизмов Вы должны видеть примерно это.
Вот, симпатишный паркетный слоник, натертый до блеска. Хм... А что если его поджечь? Попробуем! =)
Изменим наш проход так, чтобы он рисовался в текстуру. Для этого добавим в эффект специальную текстуру – Renderable Texture. Кликнем правой кнопкой мыши на эффекте и выберем пункт Add Texture -> Add Renderable Texture. Чтобы теперь проход рисовался в эту текстуру добавим в него Render Target кликнув на нем правой кнопкой мыши и выбрав Add Render Target -> renderTexture.
Для получения конечного результата нам нужно добавить в эффект второй проход. Кликнем правой кнопкой мыши на эффекте и выберем Add Pass. Также наш второй проход должен отрисовывать текстуру на весь экран, для этого добавим в эффект новую модель, кликнув на нем правой кнопкой мыши и выбрав пункт Add Model -> ScreenAlignedQuad.3ds. Теперь рассмотренными выше способами изменим ссылку Model на нашу вновь добавленную модель. Получится примерно такая картина:
Также добавим 2D-текстуру distortion.tga в эффект и текстурный объект noise на нее ссылающийся в проход Pass 1. Добавим также в этот проход тектстурный объект elephant ссылающийся на тектуру renderTexture созданную в первом проходе. Еще добавим два своих параметра типа Float:
distortPower = 0.0532; ( коэффициент искажения )
hazeSpeed = 0.05; ( скорость движения текстуры искажения )
И один Predefined параметр fTime0_X; ( прошедшее время )
Все, необходимые параметры заданы. Теперь немного теории: в первом проходе мы отрисовываем сцену в текстуру. Во втором проходе нам нужно ее исказить по формуле:
result = tex2D( RT, newTC ); newTC = tex2D( noise, T * speed ) * P + TC; |
Где RT – Render Target текстура заполняющаяся в первом проходе.
noise – шумовая текстура.
T – прошедшее время.
speed – скорость перемещения шумовой текстуры.
P – коэффициент искажения.
TC – оригинальные текстурные координаты.
Теперь запрограммируем шейдеры:
Vertex Program:
varying vec2 tc0; void main(void) { vec2 screenPos = sign( gl_Vertex.xy ); gl_Position = vec4( screenPos, 0.0, 1.0); tc0 = screenPos * 0.5 + 0.5; } |
Здесь мы располагаем наш квадрат так, чтобы он занимал весь экран. Функция sign(x) возвращает следующие значения:
-1 если x < 0 0 если х = 0 1 если x > 0
И переводим текстурные координаты из [-1,1] в [0,1].
Fragment Program:
uniform sampler2D elephant; uniform sampler2D noise; uniform float distortPower; uniform float fTime0_X; uniform float hazeSpeed; varying vec2 tc0; void main(void) { vec4 n = 2.0 * texture2D( noise, tc0 + vec2( 0.0, fTime0_X ) * hazeSpeed ) - 1.0; vec2 newTC = n.xy * distortPower + tc0; gl_FragColor = texture2D( elephant, newTC, 2.0 ); } |
Здесь мы всего лишь запрограммировали рассмотренную выше формулу.
Если Вы сделали все правильно, то у Вас должно получиться нечто такое:
Проект RenderMonkey:
OGL_HeatHaze.zip (1954)
Огромное спасибо за статью! очень помогла с пониманием шейдеров!
уааа, вы даже не представляете, как я рад тому, что у меня получается )
Очень рад что смог Вам помочь.
Желаю успехов в освоении удивительного мира компьютерной 3D-графики.
Да, действительно, отличная статья. Для тех, кто не знаком с RenderMonkey, может послужить хорошим вводом. Жаль, только, что GLSL'у и матану, окружающему его, в несколько кликов не научишься
А можете посоветовать как GLSL изучать ?
Как и любой другой язык - больше на нем писать.
В интернете куча статей и примеров интересных эффектов.
Просто пишите, и заметите что с каждым разом вы все больше и больше пишете сами, не подглядывая.
Не бойтесь экспериментировать.