WebGL Урок 1 — Треугольник и квадрат

Урок 2 >> << Урок 0

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

Добро пожаловать на мой первый учебник по WebGL! Этот первый урок основан на Урок 2 учебника NeHe по OpenGL — популярному ресурсу для изучения 3D-графики для разработки игр. Он показывают, как нарисовать треугольник и квадрат на веб-странице. Возможно, этот урок не дико захватывающий, но он — отличное введение в основы WebGL. Если вы поймете, как оно работает, остальное должно быть довольно простым…

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

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

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

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

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

Как и многие программы, эта страница WebGL начинается с определения пучка низкоуровневых функций, которые используются высокоуровневым кодом в конце текста. Для объяснения я начну с конца текста и буду продвигаться вверх, поэтому следуйте за мной вниз.

Вы увидите следующий код HTML:


<body onload="webGLStart();">
  <a href="http://webgl.lo/WebGL Урок 1 — Треугольник и квадрат/"><< Обратно к первому уроку</a><br />

    <canvas id="lesson01-canvas" style="border: none;" width="500" height="500"></canvas>

    <br/>
    <a href="http://webgl.lo/WebGL Урок 1 — Треугольник и квадрат/"><< Обратно к первому уроку</a><br />
</body>

Это и все тело страницы — все остальное находится в JavaScript (хотя если вы посмотрите код через «Исходный код страницы», вы увидите немного дополнительного хлама для нужд аналитики веб-сайта, его можно просто проигнорировать). Очевидно, мы бы могли поместить более привычный код HTML в тэг <body> и встроить наше WebGL-изображение в обычную веб-страницу, но для нашего простого примера мы просто добавили ссылки обратно на блог и элемент <canvas>, где живет 3D-графика. Элемент canvas появился в HTML5, и с его появлением добавилась возможность создания графических элементов с помощью JavaScript — и 2D, и 3D (через WebGL). Для тэга <canvas> мы не указываем ничего, кроме простых свойств разметки, и вместо этого помещаем весь код инициализации WebGL в функцию JavaScript, которая называется webGLStart и которая вызывается один раз при загрузке страницы.

Давайте поднимемся до этой функции и взглянем на нее:


  function webGLStart() {
    var canvas = document.getElementById("lesson01-canvas");
    initGL(canvas);
    initShaders();
    initBuffers();

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.enable(gl.DEPTH_TEST);

    drawScene();
  }

Она, в свою очередь, вызывает функции для инициализации WebGL и шейдеров, которые я упоминал ранее, передавая в них параметром элемент canvas, на котором мы хотим нарисовать 3D-объекты, и затем инициализирует буферы функцией initBuffers. Буферы содержат описание треугольника и квадрата, которые мы будем рисовать — мы вскоре о них поговорим. Далее функция выполняет базовую настройку WebGL — заливает canvas черным цветом, очищая его, и включает проверку глубины (чтобы объекты, нарисованные позади других объектов, скрывались объектами, находящимися впереди них). Эти настройки выполняются через вызов методов объекта gl — мы увидим, как он инициализируется, немного позже. И, наконец, вызывается функция drawScene, которая рисует треугольник и квадрат на основании буферов.

Позже мы обязательно вернемся к initGL и initShaders, так как они важны в понимании принципов работы, но сначала взглянем на initBuffers и drawScene.

Сначала initBuffers, строчка за строчкой:


  var triangleVertexPositionBuffer;
  var squareVertexPositionBuffer;

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

Идем далее:


  function initBuffers() {
    triangleVertexPositionBuffer = gl.createBuffer();

Мы создали буфер для координат вершин треугольника. Вершины — это точки в измерении 3D, которые определяют форму, которую мы рисуем. Для нашего треугольника мы имеем три точки (которые мы установим с минуты на минуту). Этот буфер фактически является кусочком области памяти на видеокарте. Устанавливая вершины один раз при инициализации, мы затем при отрисовке сцены просто говорим WebGL «нарисуй мне то, о чем я тебе говорил ранее», и так мы можем сделать наш код действительно эффективным, особенно при анимации сцен, где объекты рисуются десятки раз в секунду для создания движения. Конечно, когда у нас всего лишь три вершины, их запись в память видеокарты не будет проблемой. Но когда вы имеете дело с большими моделями, состоящими из десятков тысяч вершин, правильное использование буферов будет существенным плюсом. Далее:


    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);

Эта строка говорит WebGL, что все последующие операции будут происходить над буфером, который мы указали. Здесь всегда есть понятие «текущий буфер», и функции работают именно с ним вместо того, чтобы каждый раз указывать, с каким буфером вам нужно работать сейчас. Немного странно, но я уверен, что на то есть причины, связанные с эффективностью…


    var vertices = [
         0.0,  1.0,  0.0,
        -1.0, -1.0,  0.0,
         1.0, -1.0,  0.0
    ];

Далее, мы определяем координаты вершин через массив JavaScript. Как вы можете видеть, это точки равнобедренного треугольника с центром в (0, 0, 0).


        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

Теперь мы создали объект Float32Array на основании нашего массива JavaScript и сказали, чтобы WebGL использовал его для заполнения текущего буфера, которым конечно же является наш triangleVertexPositionBuffer. Мы поговорим подробнее о Float32Arrays в будущих уроках, а пока все, что вам нужно знать, что это путь преобразования массива JavaScript в нечто подходящее для заполнения буфера WebGL.


    triangleVertexPositionBuffer.itemSize = 3;
    triangleVertexPositionBuffer.numItems = 3;

Последнее, что мы делаем с буфером — устанавливаем ему два новых свойства. Они не относятся явным образом к WebGL, но они будут очень полезны нам позже. Одна из хороших особенностей (кто-то скажет — плохих) JavaScript — то, что объект не должен явным образом поддерживать свойство, которое вы устанавливаете. Поэтому хотя объект буфера ранее не имел свойств itemSize и numItems, теперь эти свойства имеет. С их помощь мы говорим, что буфер из девяти элементов представляет собой три отдельные координаты вершин (numItems), каждая из которых состоит из трех чисел (itemSize).

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


    squareVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
    vertices = [
         1.0,  1.0,  0.0,
        -1.0,  1.0,  0.0,
         1.0, -1.0,  0.0,
        -1.0, -1.0,  0.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    squareVertexPositionBuffer.itemSize = 3;
    squareVertexPositionBuffer.numItems = 4;
  }

Здесь все довольно очевидно — квадрат имеет четыре вершины вместо трех и поэтому массив больше и numItems отличается.

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


  function drawScene() {
    gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);

Первое, что нам нужно сделать — немного рассказать WebGL о размерах элемента canvas, используя функцию viewport. Мы вернемся к тому, почему это важно, в (гораздо!) более поздних уроках. Сейчас вам просто нужно знать, что нужно вызывать эту функцию с размерами canvas в качестве параметров перед началом отрисовки. Затем мы очищаем canvas для подготовки к отрисовке:


    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

…и затем:


    mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);

Мы установили перспективу, с которой мы хотим обозревать сцену. По умолчанию близкие объекты будут иметь тот же размер, что и очень отдаленные (такой стиль 3D называется ортографическая проекция). Чтобы отдаленные объекты выглядели меньше, нам нужно немного рассказать о перспективе. Для этой сцены мы говорим, что наше (вертикальное) поле зрения — 45°, сообщаем отношение ширины к высоте элемента canvas и говорим, что не хотим видеть объекты ближе 0.1 единиц и дальше 100 единиц.

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

Теперь, когда мы определили перспективу, мы можем продолжить двигаться к отрисовке объектов:


    mat4.identity(mvMatrix);

Сначала нужно «переместиться» в центр 3D-сцены. В OpenGL при отрисовке сцены каждый объект отрисовывается в «текущей» позиции с «текущим» поворотом — например, вы говорите «переместись на 20 единиц прямо, повернись на 32 градуса и нарисуй робота», что в свою очередь является сложным набором инструкций «это смести, немного поверни, отрисуй». Это удобно, потому что вы можете поместить код «нарисуй робота» в функцию и затем легко устанавливать положение робота просто изменением параметров перемещения/поворота и вызовом этой функции.

Текущее положение и текущий поворот находятся в матрице. Как вы, возможно, изучали в школе, матрицы могут представлять собой переходы (перемещение из одной координаты в другую), повороты и другие геометрические трансформации. По некоторым причинам, в которые я не хочу сейчас углубляться, используется матрица размером 4х4 (не 3х3) для представления любого количество трансформаций в 3D-пространстве. Вы начинаете с единичной матрицы — да, именно так, первая матрица представляет собой преобразование, которое ничего не меняет — затем умножаете на матрицу, которая осуществляет первое преобразование, затем второе преобразование и так далее. Комбинированная матрица объединяет все преобразования в одной матрице. Матрица, которую мы используем для представления текущего состояния положения/поворота, называется модель-вид матрицей. Как вы уже наверное догадались, переменная mvMatrix содержит матрицу модель-вид, а функция mat4.identity устанавливает ее в единичную матрицу, чтобы мы могли начать умножение на переходы и повороты. Другими словами, функция переносит нас в начальную точку, из которой мы можем двигаться к отрисовке нашего 3D-мира.

Внимательный читатель заметит, что в начале обсуждения матриц я сказал «в OpenGL», не «в WebGL». Все потому, что WebGL не содержит такого функционала в графической библиотеке. Вместо этого мы используем стороннюю библиотеку матриц — замечательную glMatrix от Brandon Jones — плюс некоторые изящные ухищрения для достижения того же эффекта. Об этих ухищрениях поговорим позже.

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


    mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);

После перемещения центра нашего 3D-пространства установкой mvMatrix в единичную матрицу мы двигаем треугольник на 1.5 единицы влево (отрицательное число по оси X) и на семь единиц внутрь сцены (отдаляемся от наблюдателя, отрицательное значение по оси Z). (mat4.translate, как вы могли уже догадаться, означает «умножить данную матрицу на матрицу перехода со следующими параметрами»)

Следующим шагом будет начало непосредственной отрисовки!


    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

Итак, вы помните, что для использования одного из наших буферов, мы вызываем gl.bindBuffer для указания текущего буфера и затем вызываем код, который оперирует им. Здесь мы выбираем наш triangleVertexPositionBuffer, затем говорим WebGL, что его значения должны быть использованы как координаты вершин. Я объясню немного больше о том, как это работает, позже. Сейчас вы можете увидеть, что мы используем свойство itemSize, которое мы устанавливали на буфере, чтобы сказать WebGL, что каждый элемент в буфере содержит три числа.

Далее мы имеем:


    setMatrixUniforms();

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

Теперь WebGL имеет массив чисел, он знает, что это координаты вершин, и он знает о наших матрицах. Пора сказать, что с этим всем нужно сделать:


    gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);

Или другими словами «отрисуй треугольниками массив вершин, который я дал тебе ранее, начиная с индекса 0 в массиве и до элемента номер numItems».

После этого WebGL отобразит наш треугольник. Приступаем к квадрату:


    mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);

Мы начнем с движения нашей матрицы модель-вид на три единицы вправо. Помните, что мы уже смещены на 1.5 единицы влево и на 7 единиц отдалены от сцены, поэтому после этого движения мы будем смещены на 1.5 единицы вправо и на 7 единиц вдаль. Далее:


    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

Так мы сообщаем WebGL, что нужно использовать наш буфер квадрата для координат вершин…


    setMatrixUniforms();

…снова переносим матрицу модель-вид и матрицу проекции (чтобы изменения mvTranslate отразились в видеокарте) и наконец:


    gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);

…отрисовываем точки. Вы спросите, что такое TRIANGLE_STRIP? Что ж, дословно это «лента треугольников» :). Если точнее, то это последовательность треугольников, когда первые три вершины массива образуют первый треугольник, затем две последних вершины первого треугольника и одна следующая за ними вершина образуют второй треугольник и так далее. В нашем случае это такой сляпанный наспех способ задания квадрата. В более сложных примерах такой способ может быть очень полезен для задания сложных поверхностей треугольниками, которые аппроксимируют эти поверхности.

В любом случае, теперь мы можем закрыть функцию drawScene.


  }

Если вы забрались так далеко, то определенно готовы начать экспериментировать. Копируйте код в локальный файл — либо из GitHub, либо из онлайн-версии. В последнем случае вам понадобится index.html и glMatrix-0.9.5.min.js. Запустите пример локально и убедитесь, что он работает. Затем попробуйте изменить некоторые координаты вершин — например, установите z-координату квадрата равной 2 или -3 и посмотрите, как он становится больше или меньше при приближении и отдалении. Или попробуйте изменить пару координат и посмотрите, как объект искривляется в перспективе. Играйтесь и не обращайте на меня никакого внимания. Я подожду.

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

Вы по-прежнему со мной? Спасибо :). Давайте сразу разберем самые скучные функции. Первая из них, вызываемая в webGLStart, — это initGL. Она почти на самом верху страницы и вот еще копия для справки:


  var gl;
  function initGL(canvas) {
    try {
      gl = canvas.getContext("experimental-webgl");
      gl.viewportWidth = canvas.width;
      gl.viewportHeight = canvas.height;
    } catch(e) {
    }
    if (!gl) {
      alert("Could not initialise WebGL, sorry :-(");
    }
  }

Здесь все очень просто. Как вы могли заметить, функции initBuffers и drawScene часто обращались к объекту gl, который явно относится к «чему-то» из самого сердца WebGL. Функция берет это «что-то», которое называется контекстом WebGL, и инициализирует его через вызов контекста элемента canvas со стандартным именем контекста в качестве параметра (как вы могли догадаться, когда-нибудь имя контекста сменится с «experimental-webgl» на «webgl». Я обновлю этот урок и блог о нем, когда это случится. Подпишитесь на ленту RSS, если вы хотите узнать об этом — а также если хотите получать еженедельные новости о WebGL). Теперь, когда у нас есть контекст, мы снова воспользуемся возможностью JavaScript установить объекту любое свойство, чтобы хранить ширину и высоту элемента canvas, к которому этот объект относится. Затем мы сможем использовать эти свойства для настройки вида и перспективы в начале функции drawScene. На этом настройка контекста завершена.

После вызова initGL, функция webGLStart вызывает initShaders. Которая, конечно же, инициализирует шейдеры (спасибо, Кэп :)). Мы вернемся к ней позже, потому что сначала мы должны взглянуть на нашу матрицу модель-вид и на проекционную матрицу, которую я также упоминал ранее. Собственно, код:


  var mvMatrix = mat4.create();
  var pMatrix = mat4.create();

Итак, мы определили переменную mvMatrix для матрицы модель-вид и pMatrix для проекционной матрицы и затем присваиваем им пустые (все элементы — нули) матрицы для начала работы. Стоит сказать немного больше о проекционной матрице. Как вы помните, мы использовали функцию mat4.perspective из glMatrix для установки нашей перспективы в начале функции drawScene. Это было необходимо, потому что WebGL напрямую не поддерживает перспективу, так же, как не поддерживает и матрицу модель-вид. Но как и процесс перемещения и вращения объектов заключается в матрице модель-вид, так и процесс отображения далеких объектов пропорционально более мелкими, чем близких объектов, отлично укладывается в логику матрицы. И, как вы несомненно догадались, этой матрицей служит проекционная матрица. Функция mat4.perspective с полем зрения и соотношением сторон заполняет матрицу значениями, которые устанавливают нужную нам перспективу.

Хорошо, теперь мы разобрались со всем, кроме функции setMatrixUniforms, которая переносит проекционную матрицу и матрицу модель-вид из JavaScript в WebGL, и кроме этих пугающих шейдеров. Они взаимосвязаны, поэтому давайте начнем с истоков.

Итак, что такое шейдер, спросите вы? Возможно, однажды в истории развития 3D-графики они могли означать то, что они и значат в переводе — как затенить или раскрасить части сцены перед прорисовкой. Однако, с течением времени ситуация изменилась и теперь они могут быть определены, как участки кода, которые могут делать все, что угодно с частями сцены перед отрисовкой. И, вообще-то, это весьма полезно, потому что а) они выполняются на видеокарте, поэтому все, что они делают, они делают действительно быстро и б) трансформации, которые они могут выполнять, могут быть весьма удобными даже в простом примере как этот.

Причина, по которой мы используем шейдеры в довольно простом примере WebGL (шейдеры идут минимум с середины учебников по OpenGL) — доступ к среде WebGL, которая выполняется в видеокарте, чтобы применить проекционную матрицу и матрицу модель-вид к нашей сцене без необходимости проходить каждую точку и каждую вершину в (относительно) медленном JavaScript. Это необыкновенно полезно и стоит накладных расходов.

И вот как они устанавливаются. Как вы помните, функция webGLStart вызывает initShaders, поэтому давайте пройдемся по ней строка за строкой:


  var shaderProgram;
  function initShaders() {
    var fragmentShader = getShader(gl, "shader-fs");
    var vertexShader = getShader(gl, "shader-vs");

    shaderProgram = gl.createProgram();
    gl.attachShader(shaderProgram, vertexShader);
    gl.attachShader(shaderProgram, fragmentShader);
    gl.linkProgram(shaderProgram);

    if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
      alert("Could not initialise shaders");
    }

    gl.useProgram(shaderProgram);

Как видите, здесь используется функция getShader, которая получает две вещи — фрагментный шейдер (fragmentShader) и вершинный шейдер (vertexShader), а затем прикрепляет их к третьей вещи, называемой программой. Программа — это код, который живет в WebGL — можете считать, что это способ описания чего-то такого, что исполняется на видеокарте. Как вы могли ожидать, вы можете прикрепить к ней множество шейдеров, каждый из которых вы можете расценивать, как фрагмент кода внутри этой программы. В частности, каждая программа может содержать один фрагментный шейдер и один вершинный шейдер. Вскоре мы посмотрим на них.


    shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
    gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);

После того, как функция установила программу и прикрепила шейдеры, она получает ссылку на «атрибут», который она хранит в новом поле объекта программы, называемом vertexPositionAttribute. Здесь снова мы используем возможность JavaScript устанавливать объекту любое свойство. По умолчанию объект программы не имеет свойства vertexPositionAttribute, но нам удобно держать два значения вместе, поэтому мы просто добавляем нужное свойство в объект программы.

Итак, для чего же нужен vertexPositionAttribute? Как вы возможно помните, мы использовали его в drawScene. Если вернуться к коду, который устанавливает вершины треугольника, вы увидите, что мы ассоциировали буфер с этим атрибутом. Скоро вы увидите, что это значит. А пока давайте обратим внимание, что мы также используем gl.enableVertexAttribArray, чтобы сказать WebGL, что мы хотим хотим передать значения для атрибута, используя массив.


    shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
    shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
  }

Последнее, что делает функция initShaders — получает еще два значение из программы, положения двух вещей, называемых uniform-переменными. Скоро мы с ними столкнемся, а пока обратите внимание, что для удобства мы сохраняем их в объекте программы.

Теперь, взгляните на getShader:


  function getShader(gl, id) {
      var shaderScript = document.getElementById(id);
      if (!shaderScript) {
          return null;
      }

      var str = "";
      var k = shaderScript.firstChild;
      while (k) {
          if (k.nodeType == 3)
              str += k.textContent;
          k = k.nextSibling;
      }

      var shader;
      if (shaderScript.type == "x-shader/x-fragment") {
          shader = gl.createShader(gl.FRAGMENT_SHADER);
      } else if (shaderScript.type == "x-shader/x-vertex") {
          shader = gl.createShader(gl.VERTEX_SHADER);
      } else {
          return null;
      }

      gl.shaderSource(shader, str);
      gl.compileShader(shader);

      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
          alert(gl.getShaderInfoLog(shader));
          return null;
      }

      return shader;
  }

Это еще одна функция, которая на самом деле гораздо проще, чем кажется на первый взгляд. Все, что мы здесь делаем — ищем на странице HTML-элемент, у которого ID равен значению параметра, получаем его содержимое, создаем фрагментный шейдер или вершинный шейдер в зависимости от его типа (о разнице между ними мы поговорим в следующих уроках) и затем передаем его в WebGL для компиляции в форму, которая может выполняться на видеокарте. Затем идет обработка ошибок — и все готово! Конечно, мы можем просто определить шейдеры в виде строк внутри кода JavaScript и не связываться с получением их из HTML. Но в HTML их гораздо удобнее читать, потому что они определены как скрипты на странице, как если бы они сами были JavaScript’ом.

Теперь взглянем на код шейдеров:


<script id="shader-fs" type="x-shader/x-fragment">
  precision mediump float;

  void main(void) {
    gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
  }
</script>

<script id="shader-vs" type="x-shader/x-vertex">
  attribute vec3 aVertexPosition;

  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;

  void main(void) {
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
  }
</script>

Первое, что нужно помнить, шейдеры не написаны на JavaScript, хотя происхождение языка очень похоже. В действительности, она написаны на специальном языке шейдеров, называемом GLSL — он многим обязан языку C (как и JavaScript, конечно).

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

Второй шейдер немного интереснее. Это вершинный шейдер, и он представляет собой часть кода видеокарты, который может делать с вершиной практически все, что угодно. Связанный с вершиной, шейдер содержит две uniform-переменные uMVMatrix и uPMatrix. Uniform-переменные очень полезны, потому что они доступны вне области шейдера — извне содержащей их программы, как вы помните из кода initShaders, где мы получали их расположение, и из кода, который мы сейчас рассмотрим, где мы присваиваем их значениям матрицы модель-вид и проекционной матрицы. Вы можете рассматривать программу шейдера как объект (в объектно-ориентированном смысле), а uniform-переменные — как поля этого объекта.

Шейдер вызывается для каждой вершины и вершина передается в код шейдера как aVertexPosition благодаря использованию vertexPositionAttribute в функции drawScene, когда мы назначили атрибут буферу. Небольшой кусочек кода в функции main шейдера просто умножает координаты вершины на матрицу модель-вид и проекционную матрицу и возвращает конечное положение вершины в качестве результата.

Итак, webGLStart вызывает initShaders, которая использует getShader для загрузки фрагментного шейдера и вершинного шейдера из скриптов веб-страницы, чтобы они были скомпилированы, переданы в WebGL и использованы в дальнейшем при отрисовке нашей 3D-сцены.

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


  function setMatrixUniforms() {
    gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix);
    gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix);
  }

Здесь, используя ссылки на uniform-переменные, которые представляют наши проекционную матрицу и матрицу модель-вид, полученные в функции initShaders, мы передаем в WebGL значения из наших матриц в JavaScript.

Фух! Довольно много для первого урока, но надеюсь, что вы (и я тоже) поняли все основы, которые понадобятся нам при построении чего-нибудь более интересного — цветных, движущихся, настоящих трехмерных моделей WebGL. Хотите узнать больше? Читайте второй урок

Урок 2 >> << Урок 0

7 thoughts on “WebGL Урок 1 — Треугольник и квадрат

  • 14.08.2016 at 13:17
    Permalink

    Странно что комментов нет, это лучшее из того что я видел по webgl на данный момент, включая англоязычные туториалы. Спасибо.

    Ответить
  • 12.12.2016 at 17:20
    Permalink

    Слишком много фраз «это объясню позже». Не знаю как остальным, но мне не удобно скакать по урокам, чтобы разобраться в конкретном моменте. Но в целом, спасибо!

    Ответить
    • 13.12.2016 at 23:47
      Permalink

      В случае с WebGL крайне тяжело вместить сразу всё в один урок, сохранив при этом адекватный объём текста, учитывая, что многие вещи заслуживают отдельной статьи. Справедливости ради скажу, что мне тоже было неудобно скакать по урокам, нить терялась, а понимание начало приходить после далеко не первого прочтения :).

      Ответить
  • 08.01.2017 at 23:19
    Permalink

    Добрый вечер! Очень интересует данная технология.
    А вот как дизайнерам разрабатывать дизайн под такие проекты?
    Дизайнеру нужно изучать языки программирования или как?
    Если не сложно можете подробно рассказать.
    Очень хочется начать использовать эту технологию на личных веб-сайтах. Но я больше специализируюсь на веб-дизайне.

    Ответить
    • 15.01.2017 at 14:31
      Permalink

      Здравствуйте!

      Язык программирования изучать совсем не обязательно. Дизайнеры обычно работают в 3D-редакторах (Blender, 3ds Max, Maya), а затем либо показывают работу прямо в этом 3D-редакторе, либо куда-то экспортируют проект — например, в видео или на веб-страницу (если, конечно, у меня верные сведения о дизайнерах 😉 ).

      Существуют и готовые решения для создания трёхмерных решений для сайтов — например, blend4web. На их сайте https://www.blend4web.com/ru масса демонстраций, мне понравилась демонстрация технологического процесса молокозавода https://www.blend4web.com/ru/demo/dairy_plant/

      Ответить
  • 21.07.2017 at 09:24
    Permalink

    Сделал все как сказано, но выскакивают ошибки:
    webgl.html:104 Uncaught ReferenceError: mat4 is not defined
    at webgl.html:104
    Uncaught ReferenceError: mat4 is not defined
    at drawScene (webgl.html:147)
    at webGLStart (webgl.html:176)
    at onload (webgl.html:185)

    Ответить

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

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