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

Эта статья является частью моей текущей серии руководств по ThreeJS средней сложности. Я давно хотел что-то среднее между уровнями вступления Как нарисовать куб и Давайте заполним экран шейдерным безумием. Итак, вот оно.

Вот изображение того, что мы хотим создать:

Живая демонстрация

Вы не поверите, но это обычная сфера с немного сдвинутыми вершинами. Щелкните ссылку демонстрации, чтобы увидеть ее в действии. Сумасшедшие вещи. И все же ядро ​​- это всего лишь одна строка кода.

transformed.x = position.x + sin(position.y*10.0 + time*10.0)*0.1;

Сложная часть - предоставить одну строку кода со всем остальным, что необходимо для работы.

15 Второй обзор шейдеров

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

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

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

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

Модификация шейдера

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

В простой программе с MeshLambertMaterial сфера выглядит так:

Давайте создадим материал, а затем начнем изменять содержимое вершинного шейдера.

const mat = new THREE.MeshLambertMaterial({
   color:’green’, 
   transparent:true, 
   opacity: 0.5
})

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

mat.onBeforeCompile = (shader) => {
   const token = ‘#include <begin_vertex>’
   const customTransform = `
       vec3 transformed = vec3(position);
       transformed.x = position.x + position.y/20.0;
       `
   shader.vertexShader =
         shader.vertexShader.replace(token,customTransform)
 }

Последний вершинный шейдер состоит из множества частей, обозначенных маркерами типа #include <begin_vertex>. Обычно токенbegin_vertex заменяется чем-то, что создает точку transformed из исходной точки position. Приведенный выше код заменяет токен begin_vertex нашим собственным кодом, который изменяет координату x каждой точки. Это единственное, что нам нужно изменить, чтобы получить эффект.

С этим нестандартным материалом сфера теперь выглядит так.

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

Измените основную строку модификации шейдера с этой:

transformed.x = position.x + position.y/20.0;

к этому

transformed.x = position.x + sin(position.y*10.0)*0.1;

Теперь сфера выглядит так:

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

Вау! Это большое изменение. Теперь давайте сделаем эффект анимированным. Нам нужно добавить в шейдер временную форму, а затем использовать ее в уравнении.

mat.onBeforeCompile = (shader) => {
    shader.uniforms.time = { value: 0}
    shader.vertexShader = `
         uniform float time;
         ` + shader.vertexShader
    const token = ‘#include <begin_vertex>’
    const customTransform = `
        vec3 transformed = vec3(position);
        transformed.x = position.x 
             + sin(position.y*10.0 + time*10.0)*0.1;
    `
   shader.vertexShader =
           shader.vertexShader.replace(token,customTransform)
   materialShader = shader
}

Это тот же код, что и раньше, но с добавлением временной формы. Он должен быть объявлен в самом объекте шейдера с помощью shader.uniforms.time, а также объявлен в коде вершинного шейдера как uniform float time;. Затем мы можем использовать его в уравнении, которое теперь изменяет x, используя синус y плюс время.

Весь приведенный выше код заставляет шейдер реагировать на изменение времени. Теперь нам просто нужно изменить время. В вашей функции render, которая вызывается в каждом кадре, добавьте такую ​​строку:

function render(time) {
   if(materialShader) 
   materialShader.uniforms.time.value = time/1000;
   renderer.render( scene, camera );
}

Теперь волны будут плавно спускаться по сфере.

Будет больше смысла, если вы посмотрите демо.

Исправить затенение

Осталась одна проблема. Если вы внимательно посмотрите на изображение выше, вы действительно сможете увидеть волны только на краю объекта, но не в центре. Это потому, что мы изменяем вершины, но не нормали.

К сожалению, изменение нормалей немного сложнее, особенно на сфере, поэтому мы оставим это до следующего раза, когда мы посмотрим на полный эффект воды.

Быть замеченным

Кстати, если вы работаете над классным интерфейсом WebVR, который вы хотели бы продемонстрировать прямо в Firefox Reality, дайте нам знать.