Статьи

Основы WebGL: часть I

WebGL — это 3D-рендеринг в браузере, основанный на OpenGL, который позволяет отображать 3D-контент непосредственно на HTML5-странице. В этом уроке я расскажу обо всем, что вам нужно, чтобы начать использовать этот фреймворк.


Есть пара вещей, которые вы должны знать, прежде чем мы начнем. WebGL — это API-интерфейс JavaScript, который отображает 3D-контент на холсте HTML5. Это делается с помощью двух скриптов, которые известны в «трехмерном мире» как шейдеры . Два шейдера:

  • Вершинный шейдер
  • Фрагмент шейдера

Теперь не слишком нервничайте, когда слышите эти имена; это просто причудливый способ сказать «калькулятор положения» и «выбор цвета» соответственно. Фрагментный шейдер легче понять; он просто сообщает WebGL, какого цвета должна быть точка на вашей модели. Вершинный шейдер немного более технический, но в основном он преобразует точки в ваших 3D-моделях в 2D-координаты. Поскольку все компьютерные мониторы являются плоскими 2D-поверхностями, а когда вы видите 3D-объекты на экране, они являются всего лишь иллюзией перспективы.

Если вы хотите точно знать, как работает этот расчет, вам нужно обратиться к математику, потому что он использует расширенные умножения матриц 4 x 4, которые немного выходят за рамки учебника по основам. К счастью, вам не нужно знать, как это работает, потому что WebGL позаботится о большинстве из них. Итак, начнем.


В WebGL есть множество небольших настроек, которые вы должны настраивать почти каждый раз, когда рисуете что-то на экране. Чтобы сэкономить время и сделать ваш код аккуратным, я собираюсь создать объект JavaScript, который будет содержать все «закулисные» вещи в отдельном файле. Для начала создайте новый файл с именем «WebGL.js» и поместите в него следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
function WebGL(CID, FSID, VSID){
    var canvas = document.getElementById(CID);
    if(!canvas.getContext(«webgl») && !canvas.getContext(«experimental-webgl»))
        alert(«Your Browser Doesn’t Support WebGL»);
    else
    {
        this.GL = (canvas.getContext(«webgl»)) ?
         
        this.GL.clearColor(1.0, 1.0, 1.0, 1.0);
        this.GL.enable(this.GL.DEPTH_TEST);
        this.GL.depthFunc(this.GL.LEQUAL);
        this.AspectRatio = canvas.width / canvas.height;
         
        //Load Shaders Here
    }
}

Эта функция конструктора принимает идентификаторы холста и двух шейдерных объектов. Сначала мы получаем элемент canvas и убедитесь, что он поддерживает WebGL. Если это так, то мы присваиваем контекст WebGL локальной переменной с именем «GL». Чистый цвет — это просто цвет фона, и стоит отметить, что в WebGL большинство параметров изменяются от 0,0 до 1,0, поэтому вам придется разделить значения rgb на 255. Так что в нашем примере 1.0, 1.0, 1.0, 1.0 означает белый фон со 100% видимостью (без прозрачности). Следующие две строки говорят WebGL рассчитать глубину и перспективу, чтобы объект, расположенный ближе к вам, блокировал объекты позади него. Наконец, мы устанавливаем соотношение сторон, которое рассчитывается путем деления ширины холста на его высоту.

Прежде чем мы продолжим и загрузим два шейдера, давайте напишем их. Я собираюсь записать их в HTML-файл, куда мы собираемся поместить фактический элемент canvas. Создайте файл HTML и поместите следующие два элемента сценария непосредственно перед закрывающим тегом body:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script id=»VertexShader» type=»x-shader/x-vertex»>
   
    attribute highp vec3 VertexPosition;
    attribute highp vec2 TextureCoord;
     
     
    uniform highp mat4 TransformationMatrix;
    uniform highp mat4 PerspectiveMatrix;
     
    varying highp vec2 vTextureCoord;
     
    void main(void) {
        gl_Position = PerspectiveMatrix * TransformationMatrix * vec4(VertexPosition, 1.0);
        vTextureCoord = TextureCoord;
    }
</script>
 
<script id=»FragmentShader» type=»x-shader/x-fragment»>
    varying highp vec2 vTextureCoord;
     
    uniform sampler2D uSampler;
     
    void main(void) {
        highp vec4 texelColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
        gl_FragColor = texelColor;
    }
</script>

Сначала создается вершинный шейдер, и мы определяем два атрибута:

  • позиция вершины, которая является местоположением в координатах x, y и z текущей вершины (точка в вашей модели)
  • координата текстуры; местоположение в текстурном изображении, которое должно быть назначено этой точке

Далее мы создаем переменные для преобразования и перспективных матриц. Они используются для преобразования 3D-модели в 2D-изображение. Следующая строка создает общую переменную для шейдера фрагмента, и в основной функции мы вычисляем gl_Position (конечная 2D-позиция). Затем мы присваиваем ‘текущую координату текстуры’ общей переменной.

В фрагментном шейдере мы просто берем координаты, которые мы определили в вершинном шейдере, и «выбираем» текстуру по этой координате. По сути, мы просто получаем цвет текстуры, соответствующий текущей точке нашей геометрии.

Теперь, когда мы написали шейдеры, мы можем вернуться к загрузке их в нашем файле JS. Поэтому замените «// Load Shaders Here» следующим кодом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var FShader = document.getElementById(FSID);
var VShader = document.getElementById(VSID);
 
if(!FShader || !VShader)
    alert(«Error, Could Not Find Shaders»);
else
{
    //Load and Compile Fragment Shader
    var Code = LoadShader(FShader);
    FShader = this.GL.createShader(this.GL.FRAGMENT_SHADER);
    this.GL.shaderSource(FShader, Code);
    this.GL.compileShader(FShader);
     
    //Load and Compile Vertex Shader
    Code = LoadShader(VShader);
    VShader = this.GL.createShader(this.GL.VERTEX_SHADER);
    this.GL.shaderSource(VShader, Code);
    this.GL.compileShader(VShader);
     
    //Create The Shader Program
    this.ShaderProgram = this.GL.createProgram();
    this.GL.attachShader(this.ShaderProgram, FShader);
    this.GL.attachShader(this.ShaderProgram, VShader);
    this.GL.linkProgram(this.ShaderProgram);
    this.GL.useProgram(this.ShaderProgram);
     
    //Link Vertex Position Attribute from Shader
    this.VertexPosition = this.GL.getAttribLocation(this.ShaderProgram, «VertexPosition»);
    this.GL.enableVertexAttribArray(this.VertexPosition);
     
    //Link Texture Coordinate Attribute from Shader
    this.VertexTexture = this.GL.getAttribLocation(this.ShaderProgram, «TextureCoord»);
    this.GL.enableVertexAttribArray(this.VertexTexture);
}

Ваши текстуры должны быть в четных байтовых размерах, иначе вы получите ошибку … как 2×2, 4×4, 16×16, 32×32 …

Сначала мы проверяем наличие шейдеров, а затем переходим к загрузке их по одному. Процесс в основном получает исходный код шейдера, компилирует его и присоединяет его к центральной программе шейдера. Есть функция LoadShader, которая получает код шейдера из файла HTML; мы вернемся к этому через секунду. Мы используем «программу шейдеров», чтобы связать два шейдера вместе, и это дает нам доступ к их переменным. Мы храним два атрибута, которые мы определили в шейдерах; поэтому мы можем ввести нашу геометрию в них позже.

Теперь давайте посмотрим на функцию LoadShader. Вы должны поместить это вне функции WebGL:

01
02
03
04
05
06
07
08
09
10
11
function LoadShader(Script){
    var Code = «»;
    var CurrentChild = Script.firstChild;
    while(CurrentChild)
    {
        if(CurrentChild.nodeType == CurrentChild.TEXT_NODE)
            Code += CurrentChild.textContent;
        CurrentChild = CurrentChild.nextSibling;
    }
    return Code;
}

По сути, он просто перебирает шейдер и собирает исходный код.


Для рисования объектов в WebGL вам понадобятся следующие три массива:

  • вершины ; точки, которые составляют ваши объекты
  • треугольники ; говорит WebGL, как соединить вершины в поверхности
  • координаты текстуры ; определяет, как вершины отображаются на изображении текстуры

Это называется УФ-картированием. Для нашего примера давайте создадим базовый куб. Я разделю куб на 4 вершины с каждой стороны, которые соединяются в два треугольника. давайте создадим переменную, которая будет содержать массивы куба.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
var Cube = {
    Vertices : [ // X, Y, Z Coordinates
     
        //Front
         
         1.0, 1.0, -1.0,
         1.0, -1.0, -1.0,
        -1.0, 1.0, -1.0,
        -1.0, -1.0, -1.0,
         
        //Back
         
         1.0, 1.0, 1.0,
         1.0, -1.0, 1.0,
        -1.0, 1.0, 1.0,
        -1.0, -1.0, 1.0,
         
        //Right
         
         1.0, 1.0, 1.0,
         1.0, -1.0, 1.0,
         1.0, 1.0, -1.0,
         1.0, -1.0, -1.0,
          
         //Left
          
        -1.0, 1.0, 1.0,
        -1.0, -1.0, 1.0,
        -1.0, 1.0, -1.0,
        -1.0, -1.0, -1.0,
         
        //Top
         
         1.0, 1.0, 1.0,
        -1.0, -1.0, 1.0,
         1.0, -1.0, -1.0,
        -1.0, -1.0, -1.0,
         
        //Bottom
         
         1.0, -1.0, 1.0,
        -1.0, -1.0, 1.0,
         1.0, -1.0, -1.0,
        -1.0, -1.0, -1.0
     
    ],
    Triangles : [ // Also in groups of threes to define the three points of each triangle
        //The numbers here are the index numbers in the vertex array
         
        //Front
         
        0, 1, 2,
        1, 2, 3,
         
        //Back
         
        4, 5, 6,
        5, 6, 7,
         
        //Right
         
        8, 9, 10,
        9, 10, 11,
         
        //Left
         
        12, 13, 14,
        13, 14, 15,
         
        //Top
         
        16, 17, 18,
        17, 18, 19,
         
        //Bottom
         
        20, 21, 22,
        21, 22, 23
         
    ],
    Texture : [ //This array is in groups of two, the x and y coordinates (aka U,V) in the texture
        //The numbers go from 0.0 to 1.0, One pair for each vertex
         
         //Front
          
         1.0, 1.0,
         1.0, 0.0,
         0.0, 1.0,
         0.0, 0.0,
          
         
         //Back
         
         0.0, 1.0,
         0.0, 0.0,
         1.0, 1.0,
         1.0, 0.0,
         
         //Right
         
         1.0, 1.0,
         1.0, 0.0,
         0.0, 1.0,
         0.0, 0.0,
          
         //Left
          
         0.0, 1.0,
         0.0, 0.0,
         1.0, 1.0,
         1.0, 0.0,
         
         //Top
         
         1.0, 0.0,
         1.0, 1.0,
         0.0, 0.0,
         0.0, 1.0,
         
         //Bottom
         
         0.0, 0.0,
         0.0, 1.0,
         1.0, 0.0,
         1.0, 1.0
    ]
};

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

Вам также может быть интересно, почему я набрал 24 очка (по 4 на каждую сторону), когда на кубе действительно всего восемь уникальных очков? Я сделал это, потому что вы можете назначить только одну текстурную координату для каждой вершины; так что, если бы мы поместили только 8 точек, тогда весь куб должен был бы выглядеть одинаково, потому что он обернул бы текстуру вокруг всех сторон, которых касается вершина. Но так, у каждой стороны есть свои точки, поэтому мы можем разместить разные части текстуры на каждой стороне.

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


Процедура рисования объектов в WebGL имеет много шагов; поэтому неплохо сделать функцию, упрощающую процесс. Основная идея заключается в загрузке трех массивов в буферы WebGL. Затем мы соединяем эти буферы с атрибутами, которые мы определили в шейдерах, а также с матрицами преобразования и перспективы. Далее мы должны загрузить текстуру в память и, наконец, мы можем вызвать команду draw . Итак, начнем.

Следующий код входит в функцию WebGL:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
this.Draw = function(Object, Texture)
{
    var VertexBuffer = this.GL.createBuffer();
 
    //Bind it as The Current Buffer
    this.GL.bindBuffer(this.GL.ARRAY_BUFFER, VertexBuffer);
 
    // Fill it With the Data
    this.GL.bufferData(this.GL.ARRAY_BUFFER, new Float32Array(Object.Vertices), this.GL.STATIC_DRAW);
 
    //Connect Buffer To Shader’s attribute
    this.GL.vertexAttribPointer(this.VertexPosition, 3, this.GL.FLOAT, false, 0, 0);
 
    //Repeat For The next Two
    var TextureBuffer = this.GL.createBuffer();
    this.GL.bindBuffer(this.GL.ARRAY_BUFFER, TextureBuffer);
    this.GL.bufferData(this.GL.ARRAY_BUFFER, new Float32Array(Object.Texture), this.GL.STATIC_DRAW);
    this.GL.vertexAttribPointer(this.VertexTexture, 2, this.GL.FLOAT, false, 0, 0);
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var TriangleBuffer = this.GL.createBuffer();
    this.GL.bindBuffer(this.GL.ELEMENT_ARRAY_BUFFER, TriangleBuffer);
    //Generate The Perspective Matrix
    var PerspectiveMatrix = MakePerspective(45, this.AspectRatio, 1, 10000.0);
 
    var TransformMatrix = MakeTransform(Object);
 
    //Set slot 0 as the active Texture
    this.GL.activeTexture(this.GL.TEXTURE0);
 
    //Load in the Texture To Memory
    this.GL.bindTexture(this.GL.TEXTURE_2D, Texture);
 
    //Update The Texture Sampler in the fragment shader to use slot 0
    this.GL.uniform1i(this.GL.getUniformLocation(this.ShaderProgram, «uSampler»), 0);
 
    //Set The Perspective and Transformation Matrices
    var pmatrix = this.GL.getUniformLocation(this.ShaderProgram, «PerspectiveMatrix»);
    this.GL.uniformMatrix4fv(pmatrix, false, new Float32Array(PerspectiveMatrix));
 
    var tmatrix = this.GL.getUniformLocation(this.ShaderProgram, «TransformationMatrix»);
    this.GL.uniformMatrix4fv(tmatrix, false, new Float32Array(TransformMatrix));
 
    //Draw The Triangles
    this.GL.drawElements(this.GL.TRIANGLES, Object.Trinagles.length, this.GL.UNSIGNED_SHORT, 0);
};

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

Я добавил две функции: MakePerspective() и MakeTransform() . Они просто генерируют необходимые матрицы 4×4 для WebGL. Функция MakePerspective() принимает в качестве аргументов вертикальное поле зрения, соотношение сторон, а также самые близкие и самые дальние точки. Все, что ближе 1 единицы и более 10000 единиц, не будет отображаться, но вы можете отредактировать эти значения, чтобы получить нужный эффект. Теперь давайте посмотрим на эти две функции:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
function MakePerspective(FOV, AspectRatio, Closest, Farest){
    var YLimit = Closest * Math.tan(FOV * Math.PI / 360);
    var A = -( Farest + Closest ) / ( Farest — Closest );
    var B = -2 * Farest * Closest / ( Farest — Closest );
    var C = (2 * Closest) / ( (YLimit * AspectRatio) * 2 );
    var D = (2 * Closest) / ( YLimit * 2 );
    return [
        C, 0, 0, 0,
        0, D, 0, 0,
        0, 0, A, -1,
        0, 0, B, 0
    ];
}
function MakeTransform(Object){
    return [
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        0, 0, -6, 1
    ];
}

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


Загрузка текстуры состоит из двух этапов. Сначала мы должны загрузить изображение, как в обычном приложении JavaScript, а затем преобразовать его в текстуру WebGL. Итак, начнем со второй части, так как мы уже в файле JS. Добавьте следующее в нижней части функции WebGL сразу после команды Draw:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
this.LoadTexture = function(Img){
    //Create a new Texture and Assign it as the active one
    var TempTex = this.GL.createTexture();
    this.GL.bindTexture(this.GL.TEXTURE_2D, TempTex);
     
    //Flip Positive Y (Optional)
    this.GL.pixelStorei(this.GL.UNPACK_FLIP_Y_WEBGL, true);
     
    //Load in The Image
    this.GL.texImage2D(this.GL.TEXTURE_2D, 0, this.GL.RGBA, this.GL.RGBA, this.GL.UNSIGNED_BYTE, Img);
     
    //Setup Scaling properties
    this.GL.texParameteri(this.GL.TEXTURE_2D, this.GL.TEXTURE_MAG_FILTER, this.GL.LINEAR);
    this.GL.texParameteri(this.GL.TEXTURE_2D, this.GL.TEXTURE_MIN_FILTER, this.GL.LINEAR_MIPMAP_NEAREST);
    this.GL.generateMipmap(this.GL.TEXTURE_2D);
     
    //Unbind the texture and return it.
    this.GL.bindTexture(this.GL.TEXTURE_2D, null);
    return TempTex;
};

Стоит отметить, что ваши текстуры должны иметь четные байтовые размеры, иначе вы получите ошибку; поэтому они должны быть размерами, такими как 2×2, 4×4, 16×16, 32×32 и т. д. Я добавил линию, чтобы перевернуть координаты Y просто потому, что координаты Y моего 3D-приложения были обратными, но это будет зависеть от того, что вы используете. Это связано с тем, что некоторые программы делают 0 по оси Y в верхнем левом углу, а некоторые приложения делают его левым нижним углом. Свойства масштабирования, которые я установил, просто сообщают WebGL, как изображение должно увеличиваться и уменьшаться. Вы можете поиграть с разными вариантами, чтобы получить разные эффекты, но я подумал, что они работают лучше всего.

Теперь, когда мы закончили с файлом JS, давайте вернемся к файлу HTML и реализуем все это.


Как я упоминал ранее, WebGL рендерится на элемент canvas. Это все, что нам нужно в разделе тела. После добавления элемента canvas ваша HTML-страница должна выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<html>
    <head>
        <!— Include Our WebGL JS file —>
        <script src=»WebGL.js» type=»text/javascript»></script>
        <script>
             
        </script>
    </head>
    <body onload=»Ready()»>
      <canvas id=»GLCanvas» width=»720″ height=»480″>
            Your Browser Doesn’t Support HTML5’s Canvas.
      </canvas>
       
    <!— Your Vertex Shader —>
     
    <!— Your Fragment Shader —>
     
    </body>
</html>

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
//This will hold our WebGL variable
var GL;
     
//Our finished texture
var Texture;
     
//This will hold the textures image
var TextureImage;
     
function Ready(){
    GL = new WebGL(«GLCanvas», «FragmentShader», «VertexShader»);
    TextureImage = new Image();
    TextureImage.onload = function(){
        Texture = GL.LoadTexture(TextureImage);
        GL.Draw(Cube, Texture);
    };
    TextureImage.src = «Texture.png»;
}

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

Теперь, хотя я и сказал, что в следующий раз мы рассмотрим преобразования, я не могу просто оставить вас со статическим квадратом; это не достаточно 3D. Давайте вернемся и добавим небольшое вращение. В файле HTML измените функцию onload чтобы она выглядела так:

1
2
3
4
TextureImage.onload = function(){
        Texture = GL.LoadTexture(TextureImage);
        setInterval(Update, 33);
};

Это будет вызывать функцию Update() каждые 33 миллисекунды, что даст нам частоту кадров около 30 кадров в секунду. Вот функция обновления:

1
2
3
4
function Update(){
    GL.GL.clear(16384 | 256);
    GL.Draw(GL.Cube, Texture);
}

Это довольно простая функция; он очищает экран, а затем рисует обновленный куб. Теперь перейдем к файлу JS, чтобы добавить код поворота.


Я не собираюсь полностью реализовывать преобразования, потому что я сохраню это в следующий раз, но давайте добавим вращение вокруг оси Y. Первое, что нужно сделать, это добавить переменную Rotation в наш объект Cube. Это будет отслеживать текущий угол и позволит нам продолжать увеличивать угол поворота. Итак, верх вашей переменной Cube должен выглядеть так:

1
2
3
4
var Cube = {
    Rotation : 0,
    //The Other Three Arrays
};

Теперь давайте обновим MakeTransform() чтобы включить вращение:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function MakeTransform(Object){
    var y = Object.Rotation * (Math.PI / 180.0);
    var A = Math.cos(y);
    var B = -1 * Math.sin(y);
    var C = Math.sin(y);
    var D = Math.cos(y);
    Object.Rotation += .3;
    return [
        A, 0, B, 0,
        0, 1, 0, 0,
        C, 0, D, 0,
        0, 0, -6, 1
    ];
}

Вот и все! В следующем уроке мы рассмотрим загрузку моделей и выполнение преобразований. Я надеюсь, вам понравился этот урок; не стесняйтесь оставлять любые вопросы или комментарии, которые у вас могут быть ниже.