21 февраля 2013 г.

OpenGL ES 2.0. Урок 5. Шейдер преломления света

В предыдущем уроке мы рисовали гладкую поверхности в виде бегущей волны. В этом уроке мы попробуем превратить нашу поверхность в настоящую воду. Луч света проходящий, через границу двух сред, немного изменяет свое направление. Этот эффект называют преломлением света. Пусть на глубине y=ybottom расположено дно, покрытое текстурой. Над поверхностью воды находится глаз наблюдателя, т.е. камера. Нужно выяснить, какую точку текстуры дна увидит наблюдатель. Для этого изменим направление хода луча света на обратное. Луч света будет выходить из глаза наблюдателя, преломляться на поверхности воды и попадать на дно. Точка пересечения преломленного луча и дна и будет видна наблюдателю.









Разобьем задачу на три этапа:
1. Найти входящий вектор IN.
2. Найти преломленный вектор OUT.
3. Найти точку пересечения преломленного вектора с дном.
Приступаем к решению первого этапа. Входящий вектор найти нетрудно, если известны координаты вершины и координаты камеры. В фрагментном шейдере это будет выглядеть так:
vec3 IN=v_vertex - u_camera;
где u_camera - координаты камеры в виде униформы, а v_vertex - это вершина на поверхности воды, на которую положил глаз наблюдатель.

Вектор IN не обязательно должен быть нормализован. В предыдущих уроках при расчете освещения мы уже использовали вектор: 
vec3 lookvector = normalize(u_camera - v_vertex)
Поэтому если мы хотим скомбинировать преломление с освещением можно в качестве вектора IN взять lookvector с противоположным знаком (т.е. IN = - lookvector), чтобы не создавать лишних вычислений в шейдере.

Приступаем к решению второго этапа.
Если нам известен входящий вектор IN, вектор единичной нормали N и относительный показатель преломления  на границе двух сред k находим преломленный вектор OUT по закону Снелла:



Здесь:

Создадим в фрагментном шейдере функцию myrefract, которая будет принимать на вход аргументы IN, NORMAL, k и возвращать преломленный вектор:
vec3 myrefract(vec3 IN, vec3 NORMAL, float k){
       // скалярное произведение нормали и вектора входящего луча
       float nv=dot(NORMAL,IN);
      // квадрат длины входящего вектора
      float v2 = dot(IN,IN);
      // коэффициент перед вектором нормали
      float knormal=(sqrt(((k*k-1.0)*v2)/(nv*nv)+1.0)-1.0)* nv;
      // вектор исходящего луча
      vec3 OUT = IN + (knormal * NORMAL);
      return OUT;
}
Можно перенести эту функцию из фрагментного в вершинный шейдер, при этом скорость отрисовки кадра возрастет в несколько раз, но визуальное качество жидкости ухудшится. Второй этап выполнен.
Приступаем к решению третьего этапа. Найдем точку пересечения преломленного вектора с плоскостью дна. Зададим уравнение плоскости дна, оно выглядит очень просто:
у = ybottom, где ybottom - глубина дна  
Как задать в пространстве уравнение прямой ? Нужно знать какую-нибудь точку через которую проходит прямая и вектор, приложенный к этой точке. Преломленный вектор проходит через вершину v_vetrex. Хорошо, одну точку прямой мы знаем.  Прицепим к ней вектор OUT, который мы уже нашли,  и получим параметрическое уравнение прямой линии преломленного луча света:
x = v_vertex.x +OUT.x * t
y = v_vertex.y + OUT.y * t
z = v_vertex.z +OUT.z *t
где t - параметр, т.е. любое число. Все бесконечное множество чисел t дают нам бесконечное количество точек прямой. Кто хочет узнать больше про уравнение прямой линии в параметрическом виде могут обратиться к википедии. Нам нужно найти такой параметр t, который соответствует пересечению вектора OUT с плоскостью дна. Соединим вместе уравнение плоскости дна и уравнение прямой, т.е. подставим в уравнение прямой ybottom вместо у:
ybottom = v_vertex.y + OUT.y * t 
Теперь мы знаем параметр t=(ybottom - v_vertex.y)/OUT.y
Подставляем найденный t в выражения для x и z получаем получаем координаты точки пересечения вектора OUT с дном:
xbottom = v_vertex.x +OUT.x * (ybottom - v_vertex.y)/OUT.y
zbottom = v_vertex.z +OUT.z * (ybottom - v_vertex.y)/OUT.y
Значение xbottom станет нашей текстурной координатой s, а zbottom - текстурной координатой t. Чтобы текстура не выглядела мелкой, уменьшим текстурные координаты в два раза. Получим двумерный вектор координат текстуры:
vec2 texCoord = vec2(0.5*xbottom,0.5*zbottom);
Пришло время оформить фрагментный шейдер. В нем я соединил эффект преломления с освещением:
precision mediump float;
uniform vec3 u_camera;
uniform vec3 u_lightPosition;
uniform sampler2D u_texture0;
varying vec3 v_vertex;
varying vec3 v_normal;

//функция преломления света, она должна быть объявлена до функции main
vec3 myrefract(vec3 IN, vec3 NORMAL, float k){
        // скалярное произведение нормали и вектора входящего луча
        float nv=dot(NORMAL,IN);
        // квадрат длины входящего вектора
        float v2 = dot(IN,IN);
       //коэффициент перед вектором нормали
       float knormal=(sqrt(((k*k-1.0)*v2)/(nv*nv)+1.0)-1.0)* nv;
       // вектор исходящего луча
       vec3 OUT = IN + (knormal * NORMAL);
       return OUT;
}

void main() {
       vec3 n_normal=normalize(v_normal);
       vec3 lightvector = normalize(u_lightPosition - v_vertex);
       vec3 lookvector = normalize(u_camera - v_vertex);
       float ambient=0.1;
       float k_diffuse=0.7;
       float k_specular=0.3;
       float diffuse = k_diffuse * max(dot(n_normal, lightvector), 0.0);
       vec3 reflectvector = reflect(-lightvector, n_normal);
       float specular = k_specular * pow( max(dot(lookvector,reflectvector),0.0), 40.0 );
       vec4 one=vec4(1.0,1.0,1.0,1.0);
      //цвет освещения
      vec4 lightColor=(ambient+diffuse+specular)*one;
      //входящий вектор IN это (-lookvector)
      //преломленный вектор OUT с показателем преломления 1.2
      vec3 OUT=myrefract(-lookvector, n_normal, 1.2);
      //глубина дна
      float ybottom=-1.0;
      //точка пересечения преломленного вектора и плоскости дна
      float xbottom=v_vertex.x+OUT.x*(ybottom-v_vertex.y)/OUT.y;
      float zbottom=v_vertex.z+OUT.z*(ybottom-v_vertex.y)/OUT.y;
      //координаты текстуры
      vec2 texCoord = vec2(0.5*xbottom,0.5*zbottom);
      //цвет текстуры, извлеченный из текстуры по координатам
      vec4 textureColor=texture2D(u_texture0, texCoord);
     //цвет пикселя - комбинация цвета освещения и цвета текстуры
     gl_FragColor=lightColor*textureColor;
}

Внесем в код предыдущего урока небольшие изменения. Заменим фрагментный шейдер, а также  в методе onSurfaceCreated загрузим какую-нибудь текстуру, например такую:


//создадим текстурный объект
mTexture0=new Texture(context,R.drawable.picture);
//создадим шейдерный объект
mShader=new Shader(vertexShaderCode, fragmentShaderCode);
//свяжем буфер вершин с атрибутом a_vertex в вершинном шейдере
mShader.linkVertexBuffer(vertexBuffer);
//свяжем буфер нормалей с атрибутом a_normal в вершинном шейдере
mShader.linkNormalBuffer(normalBuffer);
//свяжем текстурный объект с шейдерным объектом
mShader.linkTexture(mTexture0, null);

Запусти приложение на исполнение и получим живую воду:


Полный код урока можно скачать отсюда Ссылка

28 комментариев:

  1. На картинке к уроку все выглядело проще. но когда открыл на девайсе все выглядит просто замечательно :) Сегодня будет долгая и бессонная ночь =)

    ОтветитьУдалить
  2. Анонимный24 мая 2013 г., 1:14

    Здравствуйте!
    У меня вопрос по-поводу зеркального отражения объектов. Как это можно реализовать на openGL ES 2.0? информации почему то по этому крайне мало и я находил те же примеры на HeNe для десктопа на старые openGl.
    Это слишком сложная операция, что ее нигде нельзя найти? Как можно создать поверхность, от которой будут отражаться объекты на сцене?
    Заранее благодарен!

    ОтветитьУдалить
    Ответы
    1. Уже обсуждали кубические текстуры. См. комментарии к уроку "OpenGL ES 2.0. Урок третий-Двумерные текстуры"

      Удалить
  3. Анонимный17 июля 2013 г., 13:30

    Извините, что не по теме, но как насчёт NDK, будут ли уроки на OpenGL ES на NDK? Ведь насколько я знаю как правило на NDK на C/С++ работает быстрее чем на Java. Хотелось бы увидеть. Есть ли на NDK что-то подобное классу Matrix или же работу с матрицами придётся реализовывать самому?

    ОтветитьУдалить
    Ответы
    1. Уроки на NDK не планирую. Не вижу разницы на чем писать исходный код - на Java или C++, т.к. в обоих случаях при компиляции он будет преобразован в Dalvik-код. На скорость это никак не повлияет.

      Удалить
    2. Анонимный18 июля 2013 г., 8:53

      Сравнение выполнения на С++ и на Java http://marakana.com/forums/android/examples/96.html, хотя конечно неизвестно одинаково ли проходят opengl вызовы... Да и вообщде насколько я знаю что, C++ код на NDK исполняется ВНЕ Dalvik. Может я ошибаюсь, укажите где написано обратное...

      Удалить
    3. Нативный код написанный с помощью NDK представляет из себя скомпилированную динамическую библиотеку и никакого отношения к далвику не имеет.

      Удалить
  4. Этот комментарий был удален автором.

    ОтветитьУдалить
  5. Здравствуйте, Андрей!
    Подскажите пожалуйста как прикрутить шейдер пост-обработки например BLOOM-эффект к вашему примеру?
    Реализация самого BLOOM-эффекта к сожалению есть только на directx здесь -> http://habrahabr.ru/post/147799/
    Спасибо!

    ОтветитьУдалить
    Ответы
    1. Попробуйте так:
      извлекаем цвет пикселя из текстуры:
      vec4 textureColor=texture2D(u_texture0, texCoord);
      получаем яркость цвета пикселя:
      float bright=length(textureColor);
      если яркость меньше 0.8, то новую яркость зануляем:
      float bright_new=step(0.8,bright);
      вычисляем новый цвет для новой яркости:
      vec4 color_new=bright_new*textureColor;
      складываем старый и новый цвета:
      gl_FragColor=lightColor*(textureColor+color_new);
      Вместо функции step можно использовать функцию smoothstep, т.е. плавное отсечение по яркости. Например так:
      float bright_new=smoothstep(0.5,0.8,bright);

      Удалить
    2. 0.8 оказалось слишком маленькой величиной,т.к. максимальная длина вектора цвета =2. Кстати яркость можно считать по другому:
      float bright=textureColor.r+textureColor.g+textureColor.b;
      В этом случае максимальная величина яркости =3.

      Удалить
  6. Спасибо за формулы, но у меня совсем дошкольный вопрос, как вообще наложить этот эффект на готовую сцену?
    Мне представляется использование двух FBO(сцена со светимостью и оригинальная сцена), потом эти два FBO сводятся на КВАДЕ как две текстуры.

    ОтветитьУдалить
    Ответы
    1. Зачем для такого простого эффекта выделять светимость в отдельную текстуру ? Берете цвет пикселя оригинальной сцены и применяете формулы в шейдере.

      Удалить
  7. Вот-вот как ко всей отрендеренной сцене применить этот эффект без квада на весь экран?
    Если применять сразу, то это будет не совсем, что нужно(у меня на сцене рендерятся 1000 объектов). Я честно не понимаю как применить пост-эффект к полностью отрендеренной сцене(а мне необходимо именно к полностью отрисованной сцене).

    ОтветитьУдалить
    Ответы
    1. Рендерить в фрэмбуффер, а во втором проходе постобработка. Но для хиленьких армовых гпу это суицид)))

      Удалить
    2. Да и не понятно, что значит 1000 объектов. Обычно мерят в трианглах.

      Удалить
  8. Извините, что надоедаю.
    Вот про какой светящийся эффект я говорил http://s019.radikal.ru/i608/1308/47/16e036ddc6d9.jpg

    ОтветитьУдалить
    Ответы
    1. Как я понял из картинки нужно сначала сделать текстуру размытой. Покопавшись в литературе, нашел такой прием. Сначала вычисляем исходные координаты текстуры, затем выполняем сдвиг координаты S на малую величину и читаем цвет пикселя из текстуры по новым координатам. Так повторяем несколько раз вокруг исходной координаты S. Складываем полученные цвета и делим на их количество(т.е. вычисляем средний цвет).

      vec3 sumcolor=vec3(0.0, 0.0, 0.0);
      vec2 ds=vec2 (0.007, 0.0);
      vec2 shift_s=texcoord-3.0*ds;
      for (int i=0; i<7; i++){
      sumcolor+=texture2D(u_texture0, shift_s).rgb;
      shift_s+=ds;
      }

      gl_FragColor=vec4(sumcolor/7.0, 1.0);

      Аналогично размываем полученный цвет по координате T.

      Размазывает картинку хорошо, но fps падает в несколько раз за счет большого количества обращений к текстуре. Буду думать дальше.

      Удалить
    2. Пытался размывать тень отреендеринную в текстуру. При ядре фильтра 6х6 фпс проседало в 3 раза, а чтоб хорошо размыть ядро нужно брать 64х64. Сделал так http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter17.html . Просело в 2 раза

      Удалить
  9. А если оригинальную картинку в FBO1 потом из этого FBO1 во второй FBO2 размыть и в итоге отрендерить mix(FBO1,FBO2,0.5)?

    P.S. И я слышал, что for в шейдере - смерть GPU!

    ОтветитьУдалить
    Ответы
    1. Сделал по этому уроку http://habrahabr.ru/post/144831/ только уменьшил текстуру до 128х128 и количество проходов с 44 до 16 производительность более-менее удовлетворяет и эффект более чем красивый, но всё-равно если есть более быстрая в плане производительности реализация было-бы очень интересно посмотреть!

      Спасибо!

      Удалить
  10. Добрый день, а планируются ли уроки по работе с тенями?(Вычесленные тени, карта теней?)

    ОтветитьУдалить
    Ответы
    1. Кажется вопрос снят, разобрался. С Вычисленной тенью можно работать через FBO а первое делается через доп текстуру(Карту нормалей)

      Удалить
  11. здравствуйте! я бы хотел узнать а как читать двоичные данные (к примеру 3д модель)Здравствуйте!
    у меня проблема с чтением файлов (нигде не нашел инфу надежда на вас):
    как в InputStream (он ссылается на файл ресурсов) считать двоичные даные к примеру float и т.п. в том числе смешанные (float int и т.п. в перемешку)

    пробывал так:

    InputStream inputStream = context.getResources().openRawResource(R.drawable.fruite);
    DataInputStream dis = new DataInputStream(inputStream);
    float as = dis.readFloat(); // но тут помечает красным

    Заранее благодарю!!

    ОтветитьУдалить
    Ответы
    1. А в каком формате данные? Экспорт от куда то? Я уже реализовывал експорт из blender через obj формат

      Удалить
  12. Анонимный8 марта 2014 г., 0:29

    Здравствуйте! Подскажите пожалуйста, как повернуть объект так, чтобы от переместился относительно света? Когда я перемножаю modelMatrix на матрицу поворота объект поворачивается вместе со светом(блик на объекте движется вместе с ним).

    ОтветитьУдалить
    Ответы
    1. Как я понимаю нужно поворачивать не только сам обьект, но и его нормали, тогда будет то что нужно

      Удалить
    2. ИМХО правильно передавать в шейдер отдельную матрицу полученную путем транспонирования и обращения 3х3 подматрицы модельной матрицы с последующим домножением на нее нормали в шейдере.

      Удалить