Урок 10. Различные шейдеры в OpenGL4

20.08.2012

В предыдущем уроке мы произвели обзор языка GLSL. В этом уроке мы разберем пару конкретных примеров шейдеров.

Шейдеры

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

Шейдер для эффекта bump

Эффект bump широко используется  для детализации поверхности модели текстурой выпуклостей, еще называемой normalmap. Шейдер для этого эффекта на рисунке имеет шар под номером два. Рассмотрим код пиксельного шейдера этого эффекта:

//------------------------------------------------------------------------------------
// Pixel Shader
//------------------------------------------------------------------------------------
#version 330
 
in vec3 theLightVec0;
in vec3 theLightVec1;
in vec3 theNormal;
in vec3 theNormalR;
in vec2 theTex;
out vec4 outputColor;
 
uniform sampler2D gSampler;
 
void main()
{
      vec2 tex=vec2(theTex.y,theTex.x);
      vec4 bump=texture2D(gSampler, tex*vec2(4,0.5)+vec2(0,0));
      vec4 color=texture2D(gSampler, theTex*vec2(1,0.5));
      vec3 rotatenormal=normalize(theNormal+theNormalR*(bump.x-0.5)*4.0);
      float lit0=max(0,dot(rotatenormal,theLightVec0));
      float lit1=max(0,dot(rotatenormal,theLightVec1));
      outputColor=color*lit0*vec4(1,1,0.5,1)+color*lit1*vec4(0,0.5,1,1);
}

Основной расчет освещения для этого эффекта ведется в пиксельномшейдере, а не в вершинном. Известно, что само по себе освещение объекта выполняется формулой dot(Normal,Pos-LightPos). Поэтому зададимся вопросом, а если имеется фактура поверхности, то есть поверхность имеет выпуклость, что изменяет эта выпуклость? На самом деле, если поверхность неровная, значит и нормали её неровные. Следовательно,

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

В данном шейдере так и сделано. Здесь используется два источника освещения, соответственно имеется два вектора от вершин объекта к источникам освещения: theLightVec0 и theLightVec1. Теперь приступим к расчету измененных нормалей. Коэффициент для варьирования берется непосредственно из текстуры bump и он равен (bump.x-0.5)*4.0. Таким образом, формула расчета освещения получается почти равная исходной — dot(rotatenormal,theLightVec0), единственное отличие её в том, что вместо исходного вектора Normal мы поставили варьированный вектор.

Шейдер для эффекта металлической поверхности

Следующий шейдер имеет более интересный код а также более эффектный вид, хотя первое и второе напрямую не связано. Во первых, нужно сказать пару слов о том, чем металлическая поверхность отличается от обычной. Для металлических предметов характерен блеск. Это происходит потому, что кристаллическая решетка металлов плотная и таким образом, отражает свет и не пропускает его через себя. Таким образом, металлическая поверхность является зеркалом. Единственное отличие – это то, что металл может быть шероховатым, и чем больше шероховатость, тем меньше металл блестит. Однако зеркальные свойства сохраняются, только в очень рассеянном виде.

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

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

//------------------------------------------------------------------------------------
// Vertex Shader
//------------------------------------------------------------------------------------
#version 330
 
uniform mat4 mWorld;
uniform mat4 mView;
uniform mat4 mProjection;
uniform vec3 vLightPos[5];
uniform vec3 vLightColor[5];
 
layout (location = 0) in vec3 inPosition;
layout (location = 1) in vec3 inNormal;
layout (location = 2) in vec2 inTex;
 
out vec3 theColor;
out vec3 theViewVec;
out vec3 theNormal;
out vec2 theTex; 
 
void main()
{
      gl_Position = mProjection*mView*mWorld*vec4(inPosition, 1.0);
      vec3 outputColor = vec3(0,0,0);
      theViewVec = normalize(vLightPos[3]-inPosition);          
 
      for (int i=0; i<2; i++)
      {
      float a=dot(normalize(inNormal),normalize(vLightPos[i]-inPosition));
      float b=dot(normalize(inNormal),normalize(theViewVec));
      float l=1.0+0.2*(length(vLightPos[i]-inPosition)+length(theViewVec));
      float c=1.0f/(1.0f+(a-b)*(a-b)*5*l);
      c=pow(c,0.5);     outputColor+=vLightColor[i]*c*0.15;
      c=pow(c,5.0);     outputColor+=vLightColor[i]*c*0.85;
      }
      theColor=outputColor;
 
      theNormal = inNormal;
      theTex = inTex;
}

Разберем код вершинного шейдера для эффекта металла. Переменная, хранящая интенсивность цвета здесь – outputColor. Эта переменная наполняется в два прохода: каждый проход для одного из источников освещения. Каждый источник освещения может иметь свой цвет. Основная операция для освещения здесь – вычисление коэффициентов a и b. Коэффициент aобозначает угол между нормалью и источником освещения. Коэффициент bобозначает угол между нормалью и точкой наблюдателя. Если углы совпадают, и эти два числа равны значит угол падения равен углу отражения, и в такой точке металл должен обладать ярким свечением. Если коэффициенты различаются, значит углы разные и источник света в данной точке и под данными углами либо не отражается совсем, либо отражается лишь слегка.

Теперь разберем пиксельныйшейдер для нашего эффекта:

//------------------------------------------------------------------------------------
// Pixel Shader 0
//------------------------------------------------------------------------------------
#version 330
 
in vec3 theColor;
in vec3 theViewVec;
in vec3 theNormal;
in vec2 theTex;
out vec4 outputColor;
 
uniform sampler2D gSampler;
 
void main()
{
      vec3 reflectDir=normalize(reflect(normalize(theViewVec),normalize(theNormal)));
      vec2 tex=vec2((asin(reflectDir.y)/1.571+1.0)*0.5,
                    (asin(-reflectDir.x)/1.571+1.0)*0.25);
      tex.x+=0.25*(1.0+reflectDir.z*20.0/(1.0f+abs(reflectDir.z*20.0))); 
 
      vec4 color=texture2D(gSampler, tex)*0.65f;
       outputColor= vec4(theColor,0.0f)+color;
}

Так как эффект блеска источников освещения уже рассчитан, то, вообщем, мы могли бы просто сделать outputColor=theColor; Однако металл может отражать и что-то еще кроме источников света, поэтому в этом примере добавлена карта отражения, представленная в виде текстуры. Для того, чтобы осуществить выборку из текстуры нужно предварительно рассчитать координаты для выборки. Для этого рассчитывается вектор reflectDir встроенной функцией reflect. В общем, наш пиксельный шейдер для металлической поверхности получился компактным и прозрачным. Это произошло благодаря тому, что большая часть эффекта металлической поверхности была рассчитана в вершинномшейдере.

Использование нескольких шейдеров в приложении

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

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

   shVertex.loadShader("data\shaders\shader2.vert", GL_VERTEX_SHADER); 
   shFragment.loadShader("data\shaders\shader2.frag", GL_FRAGMENT_SHADER); 
 
   spMain.createProgram(); 
   spMain.addShaderToProgram(&shVertex); 
   spMain.addShaderToProgram(&shFragment); 
 
   spMain.linkProgram(); 
 
   shVertex2.loadShader("data\shaders\shader.vert", GL_VERTEX_SHADER); 
   shFragment2.loadShader("data\shaders\shader.frag", GL_FRAGMENT_SHADER); 
 
   spMain2.createProgram(); 
   spMain2.addShaderToProgram(&shVertex2); 
   spMain2.addShaderToProgram(&shFragment2);
 
   spMain2.linkProgram();

В приложении загружается два объекта мешей типа torus, они загружаются в объекты Mesh1 и Mesh2 соответственно. При рендере каждому объекту назначается свойшейдер:

   // Установка шейдера 1
   spMain.useProgram();
   // Рендер меша 1
   Mesh1->Draw();
   // Установка шейдера 2
   spMain2.useProgram();
   // Рендер меша 2
   Mesh2->Draw();

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

Обзор приложения

Приложение для данного урока и исходные тексты приложения и шейдеров вы можете скачать тут. Как всегда, вы можете найти в папке с исходными кодами и само скомпилированное приложение.

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

Заключение

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

Заполнен: OpenGL4
Присвоен тэг:

avatar

Об Авторе ()

Планета назначения, JO - 8703 - IV, обозначается светлой точкой на объемном экране корабля. К тому времени, как Робоид нанялся на юпитерианский круговой рейс, ему следовало бы стать главным инженером, но после Суховодного пионерского его вышибли, внеся в черный список, и высадили в Луна-Сити за то, что вместо слежения за приборами он провел время, программируя синтезатор пищи корабля строжайше запрещенной корабельными инструкциями последовательностью синтеза пива.

Комментирование закрыто.

Наверх