WebGL Урок 8 — Буфер глубины, прозрачность и смешивание

Урок 9 >> << Урок 7

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

Добро пожаловать на мой восьмой урок по WebGL, основанный на Урок 8 учебника NeHe по OpenGL. В нем мы рассмотрим смешивание и в качестве полезного побочного эффекта затронем работу буфера глубины.

Вот как выглядит результат урока в браузере с поддержкой WebGL (к сожалению, прозрачность на видео видно не очень хорошо):

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

Под элементом canvas находятся флажок для включения и отключения смешивания и эффекта прозрачности. Кроме того, вы можете задавать степень прозрачности (которую мы рассмотрим позже) и, конечно же, регулировать освещение.

Теперь о том, как это все работает…

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

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

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

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

Буфер глубины

Когда вы говорите WebGL, чтобы он что-то отрисовал, он последовательно проходит по этапам конвейера визуализации, который мы рассматривали во втором уроке. Если рассматривать сверху, он:

  1. Запускает вершинный шейдер на всех вершинах, чтобы вычислить, что где находится.
  2. Производит линейную интерполяцию между вершинами, что определяет, какие фрагменты (вы можете считать их пикселями) должны быть раскрашены.
  3. Для каждого фрагмента запускаем фрагментный шейдер, чтобы вычислить его цвет.
  4. Записываем это во фреймбуфер.

А фреймбуфер — это то, что отображается на экране. Но что, если вы отрисовываете два объекта? Например, что если вы отрисовываете квадрат с центром в (0, 0, -5), а затем второй квадрат того же размера в (0, 0, -10)? Вы бы не хотели, чтобы второй квадрат перекрыл первый, потому что он явно дальше от нас и должен быть скрыт.

Подобные ситуации WebGL обрабатывает с помощью буфера глубины. Когда фрагменты вместе с RGBA-значениями цвета после работы фрагментного шейдера записываются во фреймбуфер, он также сохраняет значение глубины, которое имеет отношение (но не является одним и тем же) к Z-координате фрагмента. (Неудивительно, что буфер глубины часто называют Z-буфером.)

Что я имею ввиду под «имеет отношение»? WebGL предпочитает, чтобы все значения Z были в диапазоне от 0 до 1, где 0 — самые близкие, а 1 — самые далекие. Это все сокрыто от нас за проекционной матрицей, которую мы создаем вызовом функции perspective в конце функции drawScene. Сейчас все, что вам нужно знать, — это то, что чем больше значение Z-буфера, тем дальше находится объект. То есть противоположно привычным координатам.

Итак, буфер глубины. Вы можете вспомнить, что в коде инициализации контекста WebGL в первом уроке у нас была такая строчка:


    gl.enable(gl.DEPTH_TEST);

Это указание системе WebGL, что делать при записи нового фрагмента во фреймбуфер, и оно по существу означает «учитывай буфер глубины». Оно идет в комбинации с другой настройкой WebGL — функцией глубины. Она имеет целесообразное значение по умолчанию, но если бы мы захотели сами установить значние по умолчанию, мы бы сделали так:


    gl.depthFunc(gl.LESS);

Это означает «если наш фрагмент имеет значение Z меньше, чем уже находящийся там фрагмент, то нужно взять наш новый фрагмент и заменить им старый». Такая проверка дает нам благоразумное поведение: объекты, которые ближе, перекрывают объекты, которые дальше. (Вы можете также устанавливать функции глубины другие значения, хотя я подозреваю, что они используются гораздо реже).

Смешивание

Смешивание — просто альтернатива этому процессу. В случае проверки глубины мы используем функцию глубины для определения того, заменять или не заменять существующий фрагмент новым фрагментом. При смешивании мы используем функцию смешивания для объединения цветом как из текущего фрагмента, так и из нового, чтобы получился совершенно новый фрагмент, который мы затем записываем в буфер.

Давайте теперь взглянем на код. Почти весь код остался таким же, как и в седьмом уроке. А почти все важные изменения находятся в коротком фрагменте в функции drawScene. Первым делом мы проверяем, включен ли флажок смешивания (Use blending).


    var blending = document.getElementById("blending").checked;

Если включен, мы устанавливаем функцию, которая будет объединять два фрагмента:


    if (blending) {
      gl.blendFunc(gl.SRC_ALPHA, gl.ONE);

Параметры этой функции определяют, как именно будет проходить смешивание. Это кропотливо, но не сложно. Для начала давайте дадим два определения: исходный фрагмент — это тот, который мы отрисовываем сейчас, а целевой фрагмент — это тот, который уже находится во фреймбуфере. Первый параметр функции gl.blendFunc определяет исходный множитель, а второй — целевой множитель. Эти числовые множители используются в функции смешивания. Ими мы говорим, что исходным множителем является значение прозрачности исходного фрагмента, а целевой множитель равняется постоянному значению — единице. Есть и другие варианты. Например, если вы используете SRC_COLOR для определения исходного цвета, исходный множитель задается отдельно для значений красного, зеленого, синего цветов и альфа-канала, который равны исходным компонентам RGBA.

Теперь давайте представим, что WebGL пытается вычислить цвет из значений RGBA целевого фрагмента (Rd, Gd, Bd, Ad) и исходного фрагмента (Rs, Gs, Bs, As).

Вдобавок, мы имеем исходные множители RGBA (Sr, Sg, Sb, Sa) и целевые множители (Dr, Dg, Db, Da).

Расчет компонентов цвета в WebGL будет происходить следующим образом:

  • Rresult = Rs * Sr + Rd * Dr
  • Gresult = Gs * Sg + Gd * Dg
  • Bresult = Bs * Sb + Bd * Db
  • Aresult = As * Sa + Ad * Da

Исходя из этого, в нашем случае мы получим (расчет для упрощения идет только для красной составляющей):

  • Rresult = Rs * As + Rd

Обычно это не идеальное решение для создания прозрачности, но оно оказывается очень неплохим в этом случае, когда освещение включено. И это стоит отдельно подчеркнуть: смешивание — это не то же самое, что прозрачность, это просто одна из техник, которая может быть использована для получения эффекта прозрачности. До меня это доходило достаточно долго, пока я работал над уроками NeHe, поэтому простите меня, если я переусердствовал в объяснении :).

Хорошо, двигаемся дальше:


      gl.enable(gl.BLEND);

Здесь все просто. Как и многие вещи в WebGL, смешивание выключено по умолчанию, поэтому нам необходимо его включить.


      gl.disable(gl.DEPTH_TEST);

Это уже немного интереснее. Мы должны отключить проверку глубины. Если мы этого не сделаем, то смешивание в некоторых случаях будет работать, а в некоторых — нет. Например, если мы отрисовываем заднюю сторону куба перед той, которая находится впереди, тогда задняя сторона запишется во фреймбуфер, а затем при записи передней стороны произойдет смешивание и мы получим то, что нужно. Однако, если мы отрисовываем переднюю сторону перед задней, то отрисовка задней стороны будет отменена при проверке глубины перед применением функции смешивания, поэтому останется лишь передняя сторона без изменений. А это явно не то, что нам нужно.

Наблюдательные читатели сделают из этого (и из функции смешивания выше) вывод, что есть строгая зависимость смешивания от порядка отрисовки объектов, но об этом позже. Давайте сначала разберем этот кусок кода:


      gl.uniform1f(shaderProgram.alphaUniform, parseFloat(document.getElementById("alpha").value));

Здесь мы получаем значение альфа-канала из текстового поля страницы и передаем его шейдеру. Все из-за того, что используемое нами изображение не имеет своего альфа-канала (просто RGB, подразумевается значение 1 альфа-канала для каждого пикселя), поэтому будет удобно задавать значение альфа-канала и смотреть на получаемое изображение.

Оставшийся код в drawScene необходим для отрисовки сцены в обычном режиме, когда смешивание выключено:


    } else {
      gl.disable(gl.BLEND);
      gl.enable(gl.DEPTH_TEST);
    }

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


  precision mediump float;

  varying vec2 vTextureCoord;
  varying vec3 vLightWeighting;

  uniform float uAlpha;

  uniform sampler2D uSampler;

  void main(void) {
     vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
     gl_FragColor = vec4(textureColor.rgb * vLightWeighting, textureColor.a * uAlpha);
  }

И это все изменения в коде!

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

Причина в том, что при исходном освещении задняя сторона куба всегда подсвечена немного. Это значит, что значения R, G и B будут низкими, поэтому при вычислении выражения

  • Rresult = Rs * Ra + Rd

они видны не так сильно. Другими словами, из-за света задние части менее видны. Если мы перенесем освещение, то менее видны станут передние части и наш эффект прозрачности будет работать не так хорошо.

Так как же нам получить «настоящую» прозрачность? На это OpenGL FAQ нам отвечает, что нужно устанавливать исходный множитель в SRC_ALPHA, а целевой множитель в ONE_MINUS_SRC_ALPHA. Но у нас снова есть проблема с тем, что исходный и целевой фрагмент обрабатываются по-разному, и поэтому остается зависимость от порядка отрисовки. И этот момент приводит нас к секрету прозрачности в OpenGL/WebGL. Процитируем тот же OpenGL FAQ:

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

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

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

Если у вас есть вопросы, комментарии или поправки, прошу оставлять их в комментариях ниже! Лично для меня это было самой сложной частью уроков NeHe, чтобы действительно все понять. И я надеюсь, что у меня получилось сделать все достаточно понятным и прозрачным.

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

Урок 9 >> << Урок 7

5 thoughts on “WebGL Урок 8 — Буфер глубины, прозрачность и смешивание

  • 16.04.2017 at 08:56
    Permalink

    А почему прозрачность сохраняется при вращении куба? Грани рисуются в одном и том же порядке, а куб поворачивается. Это значит, целевая грань то спереди, то сзади, почему же эффект не меняется?

    Ответить
    • 16.04.2017 at 14:19
      Permalink

      Всё дело в освещении. Передние грани лучше видно, потому что на них падает свет, их цвета получаются более насыщенными. Поэтому прозрачность выглядит довольно реалистичной.
      Попробуйте установить направленному освещению Z=1. Тогда лучше будут видны дальние грани и реалистичность пропадёт.
      Ещё попробуйте убрать освещение через соответствующую галку, все грани будет видно одинаково и реалистичный эффект прозрачности снова потеряется.

      Ответить
      • 17.04.2017 at 02:43
        Permalink

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

        Ответить
        • 17.04.2017 at 21:19
          Permalink

          Значение освещения зависит от нормалей. У задней грани нормаль смотрит по направлению от наблюдателя, поэтому значение освещения для этой грани принимает отрицательное значение. Измените значения нормалей для задней грани с -1 на +1 (в переменной vertexNormals), и задняя грань станет светлой:

          // Back face
          0.0, 0.0, 1.0,
          0.0, 0.0, 1.0,
          0.0, 0.0, 1.0,
          0.0, 0.0, 1.0

          Ответить
          • 18.04.2017 at 01:07
            Permalink

            Интересно, спасибо:) То есть, оно их затемняет (освещение)

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

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