WebGL Урок 15 — Карта отражений

Урок 16 >> << Урок 14

Материал в оригинале можно найти здесь

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

Вот как выглядит результат урока в браузере с поддержкой WebGL:

Здесь можно посмотреть онлайн-демонстрацию, если ваш браузер поддерживает WebGL. Здесь можно узнать, что делать, если браузер не поддерживает WebGL. Вы увидите вращающуюся модель Земли с очень ярким бликом — отражение лучей Солнца от поверхности. Если вы присмотритесь, то заметите, что блик появляется только над океанами. Суша, как вы и могли ожидать, не отражает свет подобным образом.

Попробуйте снять галочку на «Использовать карту отражений» под элементом canvas. Вы увидите, что блики появляются и на поверхности суши тоже. У вас может сложиться впечатление (как сложилось у меня), что это поведение портит иллюзию Солнца, и блик выглядит так, будто нам светят очень ярким прожектором с поверхности Земли. Карты отражений могут значительно улучшить реализм с помощью описания того, как отдельные части вашей модели будут вести себя при воздействии света.

Теперь снова включите карту отражений, уменьшите интенсивность рассеянного освещения до, скажем, (0.5, 0.5, 0.5) и снимите галочку с «Использовать цветовую карту». Вы увидите, что текстура пропала, но появление бликов по-прежнему зависят от того, суша это или вода.

Как же это все работает? Читайте дальше и узнаете.

Уже обычное предупреждение: эти уроки ориентированы на людей с некоторым знанием программирования, но без опыта работы с 3D-графикой. С хорошим пониманием того, что происходит в коде, вы быстро начнете писать собственные 3D веб-страницы. Если вы не прочитали предыдущие уроки, возможно, вам следует сделать это перед чтением урока, где я буду объяснять лишь новые вещи. Урок основан на четырнадцатом уроке, поэтому вы должны хорошо понимать происходившие там вещи.

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

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

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

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

Это и является основным трюком в данном уроке. Две различных текстуры передаются во фрагментный шейдер для моделирования Земли. Карта цвета выглядит следующим образом:

…и изображение более низкого разрешения для карты бликов выглядит так:

Это обычный файл GIF, который я создал в Paint.NET, используя карту цветов как отправную точку. Я решил устанавливать красную, зеленую и синюю составляющую цвета в каждой точке в одинаковое значение, которое бы обозначало уровень блеска поверхности. Мне также нужно было выбрать значение, которое бы означало «этот фрагмент поверхности Земли не блестит». Из-за того, что большинство изображений были достаточно темными (блеск со значением 32 соответствует цвету RGB, равному (32, 32, 32), а это очень темный серый цвет), я решил использовать просто белый цвет.

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

Первое изменение в добавлении пары новых uniform-переменных, которые указывают, нужно ли нам использовать карту цветов и карту отражений:


  precision mediump float;

  varying vec2 vTextureCoord;
  varying vec3 vTransformedNormal;
  varying vec4 vPosition;

  uniform bool uUseColorMap;
  uniform bool uUseSpecularMap;
  uniform bool uUseLighting;

Далее, у нас появились uniform-переменные для двух текстур. Мы переименовали uSampler, которая использовалась для текстуры цвета, в uColorMapSampler, а еще добавили новую переменную для карты отражений.


  uniform vec3 uAmbientColor;

  uniform vec3 uPointLightingLocation;
  uniform vec3 uPointLightingSpecularColor;
  uniform vec3 uPointLightingDiffuseColor;

  uniform sampler2D uColorMapSampler;
  uniform sampler2D uSpecularMapSampler;

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


  void main(void) {
    vec3 lightWeighting;
    if (!uUseLighting) {
      lightWeighting = vec3(1.0, 1.0, 1.0);
    } else {
      vec3 lightDirection = normalize(uPointLightingLocation - vPosition.xyz);
      vec3 normal = normalize(vTransformedNormal);

И, наконец, мы добрались до кода, который работает с картой отражений. Сначала мы определяем переменную, которая будет содержать «количество» бликового освещения. Она будет равна нулю, если соответствующий фрагмент не будет содержать бликов после всех вычислений.


      float specularLightWeighting = 0.0;

Теперь нам нужно вычислить силу блеска для этой части материала. Если пользователь указал, что карта отражений использоваться не будет, код выбирает (практически произвольное) значение 32.0. Иначе мы получим значение из карты отражений по текстурным координатам, как раньше мы находили значение цвета из цветовой карты. Итак, в нашей карте отражений все три компонента цвета — красный, зеленый и синий — имеют одинаковое значение, поэтому при просмотре мы видим лишь оттенки серого. В следующем коде мы используем значение красной компоненты, хотя могли бы взять любую другую:


      float shininess = 32.0;
      if (uUseSpecularMap) {
        shininess = texture2D(uSpecularMapSampler, vec2(vTextureCoord.s, vTextureCoord.t)).r * 255.0;
      }

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


      if (shininess < 255.0) {

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


        vec3 eyeDirection = normalize(-vPosition.xyz);
        vec3 reflectionDirection = reflect(-lightDirection, normal);

        specularLightWeighting = pow(max(dot(reflectionDirection, eyeDirection), 0.0), shininess);
      }

И, наконец, мы учитываем вклады всех других источников освещения и рассчитываем таким образом суммарный цвет фрагмента, независимо от того, пришел ли он из карты цветов или (при uUseColorMap равной false) просто равен белому цвету.


      float diffuseLightWeighting = max(dot(normal, lightDirection), 0.0);
      lightWeighting = uAmbientColor
        + uPointLightingSpecularColor * specularLightWeighting
        + uPointLightingDiffuseColor * diffuseLightWeighting;
    }

    vec4 fragmentColor;
    if (uUseColorMap) {
      fragmentColor = texture2D(uColorMapSampler, vec2(vTextureCoord.s, vTextureCoord.t));
    } else {
      fragmentColor = vec4(1.0, 1.0, 1.0, 1.0);
    }
    gl_FragColor = vec4(fragmentColor.rgb * lightWeighting, fragmentColor.a);
  }

Что ж, если вы добрались так далеко, то на самом деле уже поняли все то, что я хотел донести до вас в этом уроке. Есть и другие изменения, но ни одно из них не заслуживает подробного рассмотрения. initShaders содержит новые uniform-переменные, initTextures загружает две новых текстуры, код для загрузки чайника заменена на функцию initBuffers, которая была в одиннадцатом уроке, drawScene отрисовывает сверу вместо чайника, а также обрабатывает значение из полей ввода и помещает их в соответствующие uniform-переменные, а функция animate заставляет Землю вращаться.

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

Урок 16 >> << Урок 14

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *