Архитектура WebGL приложения для чайников (WebGL application architecture for Dummies)

Автор: | 23.12.2018

Tags:  WebGL Basics 3D SHADERS MATRIX Dummies

Введение
Приложение WebGL-Interactive Cube
Матрицы для определения позиции вершины
Задача модификации WebGL-Interactive Cube  в приложение Рупорная антенна
Композиция элементарных преобразований в приложении WebGL-Interactive Cube
Массивы, буферы и примитивы для определения модели
Описание элементов модели через ассоциативные массивы
Полезные ссылки

Введение

Архитектура WebGL-приложения относительно сложная для новичка в программировании и 3D моделировании.  Лучший способ разобраться в этой сложности — рассмотреть хорошо структурированный простой пример приложения с минимальной достаточностью раскрываемой проблематики.  Провел поиск в сети подобного примера. Наиболее соответствует приложение  WebGL-Interactive Cube — вращение куба, используя мышь. Однако, в этом приложении, на мой взгляд, громоздко описываются аффинные преобразования. Игнорируется возможность компактно описать преобразования одной матрицей, которая представляет композицию элементарных преобразований.

На моем сайте задачи 3D-моделирования, как правило,  рассматриваются на простом примере приложения Рупорная антенна (см.  2d3d , 3D графика на основе OpenGL ES и Преобразования 3D модели на VLISP и DCL). В этой статье также рассмотрена задача модификации приложения  WebGL — Interactive Cube в приложение Рупорная антенна — с устранением указанного выше недостатка и рядом дополнений.

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

Приложение WebGL-Interactive Cube

 

Ниже приводится программный код приложения.

Файл index.html

<!doctype html>
<html>
   <body>
      <canvas width = "570" height = "570" id = "my_Canvas"></canvas>

      <script>
         /*============= Creating a canvas ======================*/
         var canvas = document.getElementById('my_Canvas');
         gl = canvas.getContext('experimental-webgl');

         /*========== Defining and storing the geometry ==========*/

         var vertices = [
            -1,-1,-1,  1,-1,-1,  1, 1,-1, -1, 1,-1,
            -1,-1, 1,  1,-1, 1,  1, 1, 1, -1, 1, 1,
            -1,-1,-1, -1, 1,-1, -1, 1, 1, -1,-1, 1,
             1,-1,-1,  1, 1,-1,  1, 1, 1,  1,-1, 1,
            -1,-1,-1, -1,-1, 1,  1,-1, 1,  1,-1,-1,
            -1, 1,-1, -1, 1, 1,  1, 1, 1,  1, 1,-1, 
         ];

         var colors = [
            1,1,1, 1,1,1, 1,1,1, 1,1,1,
            1,1,1, 1,1,1, 1,1,1, 1,1,1,
            0,0,1, 0,0,1, 0,0,1, 0,0,1,
            1,0,0, 1,0,0, 1,0,0, 1,0,0,
            1,1,0, 1,1,0, 1,1,0, 1,1,0,
            0,1,0, 0,1,0, 0,1,0, 0,1,0 
         ];

         var indices = [
            0,1,2, 0,2,3, 4,5,6, 4,6,7,
            8,9,10, 8,10,11, 12,13,14, 12,14,15,
            16,17,18, 16,18,19, 20,21,22, 20,22,23 
         ];

         // Create and store data into vertex buffer
         var vertex_buffer = gl.createBuffer ();
         gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
         gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

         // Create and store data into color buffer
         var color_buffer = gl.createBuffer ();
         gl.bindBuffer(gl.ARRAY_BUFFER, color_buffer);
         gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);

         // Create and store data into index buffer
         var index_buffer = gl.createBuffer ();
         gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, index_buffer);
         gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);

         /*=================== SHADERS =================== */

         var vertCode = 'attribute vec3 position;'+
            'uniform mat4 Pmatrix;'+
            'uniform mat4 Vmatrix;'+
            'uniform mat4 Mmatrix;'+
            'attribute vec3 color;'+//the color of the point
            'varying vec3 vColor;'+
            'void main(void) { '+//pre-built function
               'gl_Position = Pmatrix*Vmatrix*Mmatrix*vec4(position, 1.);'+
               'vColor = color;'+
            '}';

         var fragCode = 'precision mediump float;'+
            'varying vec3 vColor;'+
            'void main(void) {'+
               'gl_FragColor = vec4(vColor, 1.);'+
            '}';

         var vertShader = gl.createShader(gl.VERTEX_SHADER);
         gl.shaderSource(vertShader, vertCode);
         gl.compileShader(vertShader);

         var fragShader = gl.createShader(gl.FRAGMENT_SHADER);
         gl.shaderSource(fragShader, fragCode);
         gl.compileShader(fragShader);

         var shaderprogram = gl.createProgram();
         gl.attachShader(shaderprogram, vertShader);
         gl.attachShader(shaderprogram, fragShader);
         gl.linkProgram(shaderprogram);

         /*======== Associating attributes to vertex shader =====*/
         var _Pmatrix = gl.getUniformLocation(shaderprogram, "Pmatrix");
         var _Vmatrix = gl.getUniformLocation(shaderprogram, "Vmatrix");
         var _Mmatrix = gl.getUniformLocation(shaderprogram, "Mmatrix");

         gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
         var _position = gl.getAttribLocation(shaderprogram, "position");
         gl.vertexAttribPointer(_position, 3, gl.FLOAT, false,0,0);
         gl.enableVertexAttribArray(_position);

         gl.bindBuffer(gl.ARRAY_BUFFER, color_buffer);
         var _color = gl.getAttribLocation(shaderprogram, "color");
         gl.vertexAttribPointer(_color, 3, gl.FLOAT, false,0,0) ;
         gl.enableVertexAttribArray(_color);
         gl.useProgram(shaderprogram);

         /*==================== MATRIX ====================== */

         function get_projection(angle, a, zMin, zMax) {
            var ang = Math.tan((angle*.5)*Math.PI/180);//angle*.5
            return [
               0.5/ang,      0 ,           0,                  0,
                  0,     0.5*a/ang,        0,                  0,
                  0,        0,      -(zMax+zMin)/(zMax-zMin), -1,
                  0,        0,     (-2*zMax*zMin)/(zMax-zMin), 1 
	          ];
         }

         var proj_matrix = get_projection(40, canvas.width/canvas.height, 1, 100);
         var mo_matrix = [ 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 ];
         var view_matrix = [ 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 ];

         view_matrix[14] = view_matrix[14]-6;

         /*================= Mouse events ======================*/

         var AMORTIZATION = 0.95;
         var drag = false;
         var old_x, old_y;
         var dX = 0, dY = 0;

         var mouseDown = function(e) {
            drag = true;
            old_x = e.pageX, old_y = e.pageY;
            e.preventDefault();
            return false;
         };

         var mouseUp = function(e){
            drag = false;
         };

         var mouseMove = function(e) {
            if (!drag) return false;
            dX = (e.pageX-old_x)*2*Math.PI/canvas.width,
            dY = (e.pageY-old_y)*2*Math.PI/canvas.height;
            THETA+= dX;
            PHI+=dY;
            old_x = e.pageX, old_y = e.pageY;
            e.preventDefault();
         };

         canvas.addEventListener("mousedown", mouseDown, false);
         canvas.addEventListener("mouseup", mouseUp, false);
         canvas.addEventListener("mouseout", mouseUp, false);
         canvas.addEventListener("mousemove", mouseMove, false);

         /*=========================rotation================*/

         function rotateX(m, angle) {
            var c = Math.cos(angle);
            var s = Math.sin(angle);
            var mv1 = m[1], mv5 = m[5], mv9 = m[9];

            m[1] = m[1]*c-m[2]*s;
            m[5] = m[5]*c-m[6]*s;
            m[9] = m[9]*c-m[10]*s;

            m[2] = m[2]*c+mv1*s;
            m[6] = m[6]*c+mv5*s;
            m[10] = m[10]*c+mv9*s;
         }

         function rotateY(m, angle) {
            var c = Math.cos(angle);
            var s = Math.sin(angle);
            var mv0 = m[0], mv4 = m[4], mv8 = m[8];

            m[0] = c*m[0]+s*m[2];
            m[4] = c*m[4]+s*m[6];
            m[8] = c*m[8]+s*m[10];

            m[2] = c*m[2]-s*mv0;
            m[6] = c*m[6]-s*mv4;
            m[10] = c*m[10]-s*mv8;
         }

         /*=================== Drawing =================== */

         var THETA = 0,
         PHI = 0;
         var time_old = 0;

         var animate = function(time) {
            var dt = time-time_old;

            if (!drag) {
               dX *= AMORTIZATION, dY*=AMORTIZATION;
               THETA+=dX, PHI+=dY;
            }

            //set model matrix to I4

            mo_matrix[0] = 1, mo_matrix[1] = 0, mo_matrix[2] = 0,
            mo_matrix[3] = 0,

            mo_matrix[4] = 0, mo_matrix[5] = 1, mo_matrix[6] = 0,
            mo_matrix[7] = 0,

            mo_matrix[8] = 0, mo_matrix[9] = 0, mo_matrix[10] = 1,
            mo_matrix[11] = 0,

            mo_matrix[12] = 0, mo_matrix[13] = 0, mo_matrix[14] = 0,
            mo_matrix[15] = 1;

            rotateY(mo_matrix, THETA);
            rotateX(mo_matrix, PHI);

            time_old = time; 
            gl.enable(gl.DEPTH_TEST);

            // gl.depthFunc(gl.LEQUAL);

            gl.clearColor(0.5, 0.5, 0.5, 0.9);
            gl.clearDepth(1.0);
            gl.viewport(0.0, 0.0, canvas.width, canvas.height);
            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

            gl.uniformMatrix4fv(_Pmatrix, false, proj_matrix);
            gl.uniformMatrix4fv(_Vmatrix, false, view_matrix);
            gl.uniformMatrix4fv(_Mmatrix, false, mo_matrix);

            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, index_buffer);
            gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);

            window.requestAnimationFrame(animate); 
         }
         animate(0);
      </script>
   </body>
</html>

Матрицы для определения позиции вершины

Выделенные в приложении WebGL-Interactive Cube фрагменты кода (см. в приложении разделы SHADERS и MATRIX определяют позиции вершин модели куба. Рассмотрим алгоритмические особенности.

Вид (изображение сцены) определяется взаимным положением объектов, наблюдателя и плоскости проекций. Чтобы описать это математически необходимо прежде всего соотнести эти элементы к единой системе координат (СК). Обычно плоскость xy  СК совпадает с плоскостью проекций.

Позиция каждой вершины объекта на изображении определяется в результате перемножения 3-х матриц на вектор ее координат (см. раздел SHADERS в программе):

gl_Position = Pmatrix*Vmatrix*Mmatrix*vec4(position, 1.);

Соответствующие им матрицы proj_matrix, view_matrix и mo_matrix (см. в приложении WebGL-Interactive Cube раздел MATRIXзадают:

  • центральную проекцию, объем отсечения и масштабирование модели относительно размеров окна;
  • положение наблюдателя;
  • положение модели.

В приложении WebGL-Interactive Cube положение наблюдателя задается точкой на оси z (z=-6). Начальное положение модели задается единичной матрицей. Эффект перспективы определяется в матрице proj_matrix значением -1/F.

Матрица proj_matrix вместе со способом проецирования определяет объем, за пределами которого объекты сцены отсекаются. Для центрального проецирования это усеченная пирамида (как в нашей программе), при ортогональном проецировании – параллелепипед. Объем видимости определяется в СК, ось z которой проходит из точки наблюдения через центр сцены.

Пирамида видимости определяется параметрами angle, a, zMin, zMax (см. в приложении WebGL-Interactive Cube раздел MATRIX) :

  • углом обзора в горизонтальной плоскости;
  • отношением ширины к высоте видового экрана;
  • расстоянием от наблюдателя к ближней плоскости отсечения;
  • расстоянием от наблюдателя к дальней плоскости отсечения.


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

Задача модификации WebGL-Interactive Cube  в приложение Рупорная антенна

Поставим задачу от приложения WebGL-Interactive Cube перейти к приложению, функциональность которого отражена на рисунке.

Управление положением рупорной антенны также, как и управление кубом осуществляется при помощи курсора мышки. В отличие от приложения WebGL-Interactive Cube все преобразования заданы непосредственно  в матрице положения модели, представленной как композиция  элементарных преобразований. Как получить такую матрицу рассмотрено ниже.

Положение антенны относительно системы координат XYZ определяется  5-ю параметрами.

Начальное положение антенны —  плоскость XY СК антенны совпадает с плоскостью изображения.

Чтобы перейти от начального до конечного положения антенны, необходимо выполнить последовательность элементарных преобразований:

Матрицу композиции преобразований получаем через последовательное перемножение матриц элементарных преобразований.

Композиция элементарных преобразований в приложении WebGL-Interactive Cube

Итак, выше была получена матрица положения антенны, представленная как композиция  элементарных преобразований. Попробуем для начала использовать ее в приложении WebGL-Interactive Cube. Т.е., пока не будем создавать модель антенны. Вместо нее в первом приближении используем модель куба.

Ниже приводится программный код модифицированного приложения WebGL-Interactive Cube (изменения выделены красным цветом):

<!doctype html>
<html>
 <body>
 <canvas width = "570" height = "570" id = "my_Canvas"></canvas>

<script>
 /*============= Creating a canvas ======================*/
 var canvas = document.getElementById('my_Canvas');
 gl = canvas.getContext('experimental-webgl');

/*========== Defining and storing the geometry ==========*/

var vertices = [
 -1,-1,-1, 1,-1,-1, 1, 1,-1, -1, 1,-1,
 -1,-1, 1, 1,-1, 1, 1, 1, 1, -1, 1, 1,
 -1,-1,-1, -1, 1,-1, -1, 1, 1, -1,-1, 1,
  1,-1,-1, 1, 1,-1, 1, 1, 1, 1,-1, 1,
 -1,-1,-1, -1,-1, 1, 1,-1, 1, 1,-1,-1,
 -1, 1,-1, -1, 1, 1, 1, 1, 1, 1, 1,-1, 
 ];

var colors = [
 1,1,1, 1,1,1, 1,1,1, 1,1,1,
 1,1,1, 1,1,1, 1,1,1, 1,1,1,
 0,0,1, 0,0,1, 0,0,1, 0,0,1,
 1,0,0, 1,0,0, 1,0,0, 1,0,0,
 1,1,0, 1,1,0, 1,1,0, 1,1,0,
 0,1,0, 0,1,0, 0,1,0, 0,1,0 
 ];

var indices = [
 0,1,2, 0,2,3, 4,5,6, 4,6,7,
 8,9,10, 8,10,11, 12,13,14, 12,14,15,
 16,17,18, 16,18,19, 20,21,22, 20,22,23 
 ];

// Create and store data into vertex buffer
 var vertex_buffer = gl.createBuffer ();
 gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

// Create and store data into color buffer
 var color_buffer = gl.createBuffer ();
 gl.bindBuffer(gl.ARRAY_BUFFER, color_buffer);
 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);

// Create and store data into index buffer
 var index_buffer = gl.createBuffer ();
 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, index_buffer);
 gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);

/*=================== SHADERS =================== */

var vertCode = 'attribute vec3 position;'+
 'uniform mat4 Pmatrix;'+
 'uniform mat4 Vmatrix;'+
 'uniform mat4 Mmatrix;'+
 'attribute vec3 color;'+//the color of the point
 'varying vec3 vColor;'+
 'void main(void) { '+//pre-built function
 'gl_Position = Pmatrix*Vmatrix*Mmatrix*vec4(position, 1.);'+
 'vColor = color;'+
 '}';

var fragCode = 'precision mediump float;'+
 'varying vec3 vColor;'+
 'void main(void) {'+
 'gl_FragColor = vec4(vColor, 1.);'+
 '}';

var vertShader = gl.createShader(gl.VERTEX_SHADER);
 gl.shaderSource(vertShader, vertCode);
 gl.compileShader(vertShader);

var fragShader = gl.createShader(gl.FRAGMENT_SHADER);
 gl.shaderSource(fragShader, fragCode);
 gl.compileShader(fragShader);

var shaderprogram = gl.createProgram();
 gl.attachShader(shaderprogram, vertShader);
 gl.attachShader(shaderprogram, fragShader);
 gl.linkProgram(shaderprogram);

/*======== Associating attributes to vertex shader =====*/
 var _Pmatrix = gl.getUniformLocation(shaderprogram, "Pmatrix");
 var _Vmatrix = gl.getUniformLocation(shaderprogram, "Vmatrix");
 var _Mmatrix = gl.getUniformLocation(shaderprogram, "Mmatrix");

gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
 var _position = gl.getAttribLocation(shaderprogram, "position");
 gl.vertexAttribPointer(_position, 3, gl.FLOAT, false,0,0);
 gl.enableVertexAttribArray(_position);

gl.bindBuffer(gl.ARRAY_BUFFER, color_buffer);
 var _color = gl.getAttribLocation(shaderprogram, "color");
 gl.vertexAttribPointer(_color, 3, gl.FLOAT, false,0,0) ;
 gl.enableVertexAttribArray(_color);
 gl.useProgram(shaderprogram);

/*==================== MATRIX ====================== */

function get_projection(angle, a, zMin, zMax) {
 var ang = Math.tan((angle*.5)*Math.PI/180);//angle*.5
 return [
 0.5/ang, 0 , 0, 0,
 0, 0.5*a/ang, 0, 0,
 0, 0, -(zMax+zMin)/(zMax-zMin), -1,
 0, 0, (-2*zMax*zMin)/(zMax-zMin), 0 
 ];
 }

var proj_matrix = get_projection(40, canvas.width/canvas.height, 1, 100);
 var view_matrix = [ 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,-6,1 ];
var mo_matrix
var A = 45.0;
A = A * Math.PI / 180.0; 
var B = -45.0;
B = B * Math.PI / 180.0; 
var C = 45.0;
C = C * Math.PI / 180.0;
var h = 1.0;
var p = 0.0;
var lastX = 0 , lastY = 0;
var mouseState=false;


 //var mo_matrix = [ 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 ];

/*var mo_matrix = [Math.cos(C)*Math.cos(A), Math.sin(C)*Math.cos(B), -Math.sin(A)*Math.cos(C), 0,
 -Math.sin(C)*Math.cos(A), Math.cos(C)*Math.cos(B), Math.sin(C)*Math.sin(A), 0,
 Math.cos(B)*Math.sin(A), -Math.sin(B), Math.cos(B)*Math.cos(A), 0,
 h*Math.cos(B)*Math.sin(A), -h*Math.sin(B)+p, h*Math.cos(B)*Math.cos(A), 1] */


 /*================= Mouse events ======================*/


canvas.addEventListener("mousemove", mouseMoveEvent, false);
canvas.addEventListener("mousedown", mouseDownEvent, false);
canvas.addEventListener("mouseup", mouseUpEvent, false);
 
function mouseMoveEvent(e) {
if (mouseState==true) {
A-=(lastX-e.pageX)*0.01;
B-=(lastY-e.pageY)*0.01;
lastX = e.pageX;
lastY = e.pageY;
draw();
}
}

function mouseDownEvent(e) {
mouseState = true;
lastX = e.pageX;
lastY = e.pageY;
} 
 function mouseUpEvent(e) {
mouseState = false;
}


/*=================== Drawing =================== */


function draw() {
 
 gl.enable(gl.DEPTH_TEST);

//gl.depthFunc(gl.LEQUAL);

gl.clearColor(0.5, 0.5, 0.5, 0.9);
 gl.clearDepth(1.0);
 gl.viewport(0.0, 0.0, canvas.width, canvas.height);
 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

mo_matrix = [Math.cos(C)*Math.cos(A), Math.sin(C)*Math.cos(B), -Math.sin(A)*Math.cos(C), 0,
 -Math.sin(C)*Math.cos(A), Math.cos(C)*Math.cos(B), Math.sin(C)*Math.sin(A), 0,
 Math.cos(B)*Math.sin(A), -Math.sin(B), Math.cos(B)*Math.cos(A), 0,
 h*Math.cos(B)*Math.sin(A), -h*Math.sin(B)+p, h*Math.cos(B)*Math.cos(A), 1]


 gl.uniformMatrix4fv(_Pmatrix, false, proj_matrix);
 gl.uniformMatrix4fv(_Vmatrix, false, view_matrix);
 gl.uniformMatrix4fv(_Mmatrix, false, mo_matrix);

//gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, index_buffer);
 gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);

//window.requestAnimationFrame(animate);
 }
draw();

 </script>
 </body>
</html>

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

Массивы, буферы и примитивы для определения модели

Модель куба  (см. в приложения WebGL-Interactive Cube  раздел Defining and storing the geometry) задается через  массивы вершин, цвета и индексов, каждый из которых помещается в соответствующие буфера для передачи на GPU (Graphic Processor Unit).

С появлением GPU весь функционал с последовательной передачей и обработкой каждой вершины примитива практически перестал использоваться. Взамен GPU принимает на вход описание трехмерной сцены в виде массивов вершин и их атрибутов. По мере совершенствования GPU появилась возможность  хранить вершины и их атрибуты в различных буферах – непосредственно на GPU.

Для создания модели могут использоваться различные примитивы.

В OpenGL можно создавать либо проволочную модель (модель определяется примитивами gl.LINE_LOOP, gl.LINE_STRIP и др.), либо — поверхностную (модель определяется примитивами gl.TRIANGLES, gl.TRIANGLE_STRIP и др.).

Модель куба определена методом gl.drawElements через тип примитива gl.TRIANGLES (см. раздел Drawing в приложения WebGL-Interactive Cube).

Для создания примитивов могут использоваться 2 метода:

  1. gl.drawArrays(mode, index, count)
  2. gl.drawElements(mode, count, type, offset)

Первый рисует объекты последовательно по вершинам из буфера вершин, второй имеет дело с буфером индексов. Параметры этих методов:

  • mode: режим, указывающий на тип примитива.
  • index:  указывает, какой номер вершины в буфере вершин будет первой для примитива.
  • count: число вершин (элементов — в методе gl.drawElements ) для рисования
  • type: тип значений в буфере индексов. Может иметь значение UNSIGNED_BYTE или UNSIGNED_SHORT
  • offset: с какого индекса будет проводиться рисование

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

Итак, необходимо взамен куба создать в модифицированном приложении  модель антенны.

Обозначим (индексируем) каждую из вершин антенны:

Наиболее просто каждую из граней антенны можно  определить типом примитива gl.TRIANGLE_STRIP через вершины по схеме (змейкой), например, нижняя поверхность — набором вершин [2,3,6,7,10,11, …]. Затем продолжаем змейку, переходя на правую поверхность  […. 8,7,4,3,0, …], верхнюю — […. 1,4,5,8,9, …] и левую — […. 10, 5, 6,1, 2].

Для определения этой модели в приложении WebGL-Interactive Cube  меняем фрагмент кода из раздела Defining and storing the geometry:

/*========== Defining and storing the geometry ==========*/

//antena v9-------v8
//     v5-------v4|
//  v1--|-----v0| |
//  |   |     | | v11
//  |  v6-----|-v7
//  |         |
//  v2--------v3
var vertices = [ 0.40,   0.20, 0.0,      // v0
 -0.40,   0.20, 0.0,     // v1
 -0.40, -0.20, 0.0,      // v2
 0.40, -0.20, 0.0,       // v3

  0.20,   0.10, -0.40,    // v4
 -0.20,   0.10, -0.40,   // v5
 -0.20,  -0.10, -0.40,   // v6
  0.20,   -0.10, -0.40,   // v7

  0.20,   0.10, -0.80,  // v8
 -0.20,   0.10, -0.80,   // v9
 -0.20,  -0.10, -0.80,  // v10
  0.20,  -0.10, -0.80   // v11
 ];

var colors = [
 1,0,0, 1,0,0, 1,0,0, 1,0,0,
 1,0,0, 1,0,0, 1,0,0, 1,0,0,
 1,0,0, 1,0,0, 1,0,0, 1,0,0

];

var indices = [2,3,6,7,10,11, 8,7,4,3,0, 1,4,5,8,9, 10, 5, 6,1,2];

Получили вот такой результат:

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

Поменяем цвета окон вершин:

var colors = [
 1,0,0, 1,0,0, 1,0,0, 1,0,0,
 0,1,0, 0,1,0, 0,1,0, 0,1,0,
 0,0,1, 0,0,1, 0,0,1, 0,0,1
];

Получаем  вот такой результат:

Из этих 2-х примеров видим, что между массивами, буферами и примитивами существует взаимосвязь, какая — разберитесь самостоятельно.

Описание элементов модели через ассоциативные массивы

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

Распланируем, каким образом будем определять элементы антенны:

  • 3 прямоугольных окна :  тип примитива  gl.LINE_LOOP, индексы вершин [ 0,1,2,3] [4,5,6,7] [8,9,10,11], цвет черный.
  • 4 боковых ребра:  тип примитива  gl.LINE_STRIP, индексы вершин  [0,4,8] [1,5,9] [2,6,10] [3,7,11], цвет черный.
  • 4 боковых грани: тип примитива gl.TRIANGLE_STRIP,  индексы вершин [2,3,6,7,10,11] и [1,0,5,4,9,8] , цвет белый, индексы вершин [0,3,4,7,8,11] и [1,2,5,6,9,10] , цвет красный.

Индексы вершин соответствуют рисунку, в программе они будут в порядке очередности, начиная с 0

 

Как видим, 11 элементов антенны определяются 3-я типами примитивов. Вершины некоторых из элементов совпадают по положению,  но различны по цвету. Например, вершина под индексом 0 встречается в различных элементах  4 раза, при этом имеет 3 различных цвета и используется для 3-х различных типов примитивов. Поэтому не представляется возможным нарисовать антенну одним запуском метода gl.drawElements () или gl.drawArrays ().

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

Ниже приводится программный код для создания  элементов  модели рупорной антенны :

  • внешнего и внутреннего окон рупора (win0 и win1);
  • правого верхнего ребра (edge_right_upper);
  • нижней поверхности (surface_bottom)

Ограничимся для простоты изложения только этими элементами. Другие элементы рупора можете создать самостоятельно — по аналогии.

Обратите внимание на изменения в приложении. Они выделены красным цветом.

<!doctype html>
<html>
 <body>
 <canvas width = "570" height = "570" id = "my_Canvas"></canvas>

<script>
 /*============= Creating a canvas ======================*/
 var canvas = document.getElementById('my_Canvas');
 gl = canvas.getContext('experimental-webgl');

/*========== Defining and storing the geometry ==========*/

//antena v9------v8
//     v5------v4 |
//  v1--|-----v0| |
//  |   |     | | v11
//  |  v6-----|-v7     win1
//  |         |
//  v2--------v3     win0


var vertices, colors, indices;
 
 vertices = [ 0.40, 0.20, 0.0, // v0
 -0.40, 0.20, 0.0, // v1
 -0.40, -0.20, 0.0, // v2
 0.40, -0.20, 0.0 // v3
 ];
 colors = [0,0,0, 0,0,0, 0,0,0, 0,0,0];

indices = [0,1,2,3];
// associative array win0
var win0 = {verts:vertices, cols:colors, inds:indices, length:4, type:gl.LINE_LOOP};

 vertices = [ 0.20, 0.10, -0.4, // v4
 -0.20, 0.10, -0.4, // v5
 -0.20, -0.10, -0.4, // v6
 0.20, -0.10, -0.4 // v7
 ];

colors = [0,0,0, 0,0,0, 0,0,0, 0,0,0];

indices = [0,1,2,3];
// associative array win1
var win1 = {verts:vertices, cols:colors, inds:indices, length:4, type:gl.LINE_LOOP};

 vertices = [ 0.40, 0.20, 0.0, // v0
 0.20, 0.10, -0.4, // v4
 0.20, 0.10, -0.8 // v8 
 ];

colors = [0,0,0, 0,0,0, 0,0,0 ];

indices = [0,1,2];

// associative array win0 edge_right_upper
var edge_right_upper = {verts:vertices, cols:colors, inds:indices, length:3, type:gl.LINE_STRIP};


 vertices = [ -0.40, -0.20, 0.0, // v2
 0.40, -0.20, 0.0, // v3
 -0.20, -0.10, -0.4, // v6
 0.20, -0.10, -0.4, // v7
 -0.20, -0.10, -0.8, // v10
 0.20, -0.10, -0.8 // v11 
 ];

colors = [1,1,1, 1,1,1, 1,1,1, 1,1,1, 1,1,1, 1,1,1];

indices = [0,1,2,3,4,5,6];

var surface_bottom = {verts:vertices, cols:colors, inds:indices, length:6, type:gl.TRIANGLE_STRIP};

function buf(element) {

// Create and store data into vertex buffer

var vertex_buffer = gl.createBuffer ();
 gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(element.verts), gl.STATIC_DRAW);

// Create and store data into color buffer
 var color_buffer = gl.createBuffer ();
 gl.bindBuffer(gl.ARRAY_BUFFER, color_buffer);
 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(element.cols), gl.STATIC_DRAW);

// Create and store data into index buffer
 var index_buffer = gl.createBuffer ();
 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, index_buffer);
 gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(element.inds), gl.STATIC_DRAW);
 
// Associating attributes to vertex shader

gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
 var _position = gl.getAttribLocation(shaderprogram, "position");
 gl.vertexAttribPointer(_position, 3, gl.FLOAT, false,0,0);
 gl.enableVertexAttribArray(_position);

gl.bindBuffer(gl.ARRAY_BUFFER, color_buffer);
 var _color = gl.getAttribLocation(shaderprogram, "color");
 gl.vertexAttribPointer(_color, 3, gl.FLOAT, false,0,0) ;
 gl.enableVertexAttribArray(_color);

gl.drawElements(element.type, element.length, gl.UNSIGNED_SHORT, 0);

}

/*=================== SHADERS =================== */

var vertCode = 'attribute vec3 position;'+
 'uniform mat4 Pmatrix;'+
 'uniform mat4 Vmatrix;'+
 'uniform mat4 Mmatrix;'+
 'attribute vec3 color;'+//the color of the point
 'varying vec3 vColor;'+
 'void main(void) { '+//pre-built function
 'gl_Position = Pmatrix*Vmatrix*Mmatrix*vec4(position, 1.);'+
 'vColor = color;'+
 '}';


 var fragCode = 'precision mediump float;'+
 'varying vec3 vColor;'+
 'void main(void) {'+
 'gl_FragColor = vec4(vColor, 1.);'+
 '}';

var vertShader = gl.createShader(gl.VERTEX_SHADER);
 gl.shaderSource(vertShader, vertCode);
 gl.compileShader(vertShader);

var fragShader = gl.createShader(gl.FRAGMENT_SHADER);
 gl.shaderSource(fragShader, fragCode);
 gl.compileShader(fragShader);

var shaderprogram = gl.createProgram();
 gl.attachShader(shaderprogram, vertShader);
 gl.attachShader(shaderprogram, fragShader);
 gl.linkProgram(shaderprogram);

/*======== Associating attributes to vertex shader =====*/
 var _Pmatrix = gl.getUniformLocation(shaderprogram, "Pmatrix");
 var _Vmatrix = gl.getUniformLocation(shaderprogram, "Vmatrix");
 var _Mmatrix = gl.getUniformLocation(shaderprogram, "Mmatrix");

/* gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
 var _position = gl.getAttribLocation(shaderprogram, "position");
 gl.vertexAttribPointer(_position, 3, gl.FLOAT, false,0,0);
 gl.enableVertexAttribArray(_position);

gl.bindBuffer(gl.ARRAY_BUFFER, color_buffer);
 var _color = gl.getAttribLocation(shaderprogram, "color");
 gl.vertexAttribPointer(_color, 3, gl.FLOAT, false,0,0) ;
 gl.enableVertexAttribArray(_color);*/

gl.useProgram(shaderprogram);

/*==================== MATRIX ====================== */

function get_projection(angle, a, zMin, zMax) {
 var ang = Math.tan((angle*.5)*Math.PI/180);//angle*.5
 return [
 0.5/ang, 0 , 0, 0,
 0, 0.5*a/ang, 0, 0,
 0, 0, -(zMax+zMin)/(zMax-zMin), -1,
 0, 0, (-2*zMax*zMin)/(zMax-zMin), 0 
 ];
 }

var proj_matrix = get_projection(40, canvas.width/canvas.height, 1, 100);
 var view_matrix = [ 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,-6,1 ];


var A = 45.0;
A = A * Math.PI / 180.0; 
var B = -45.0;
B = B * Math.PI / 180.0; 
var C = 45.0;
C = C * Math.PI / 180.0;
var h = 1.0;
var p = 0.0;
var lastX = 0 , lastY = 0;
var mouseState=false;

 /*================= Mouse events ======================*/


canvas.addEventListener("mousemove", mouseMoveEvent, false);
canvas.addEventListener("mousedown", mouseDownEvent, false);
canvas.addEventListener("mouseup", mouseUpEvent, false);
 
function mouseMoveEvent(e) {
if (mouseState==true) {
A-=(lastX-e.pageX)*0.01;
B-=(lastY-e.pageY)*0.01;
lastX = e.pageX;
lastY = e.pageY;
draw();
}
}

function mouseDownEvent(e) {
mouseState = true;
lastX = e.pageX;
lastY = e.pageY;
} 
 function mouseUpEvent(e) {
mouseState = false;
}


/*=================== Drawing =================== */


function draw() {
 
 gl.enable(gl.DEPTH_TEST);

//gl.depthFunc(gl.LEQUAL);

 gl.clearColor(0.5, 0.5, 0.5, 0.9);
 gl.clearDepth(1.0);
 gl.viewport(0.0, 0.0, canvas.width, canvas.height);
 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

mo_matrix = [Math.cos(C)*Math.cos(A), Math.sin(C)*Math.cos(B), -Math.sin(A)*Math.cos(C), 0,
 -Math.sin(C)*Math.cos(A), Math.cos(C)*Math.cos(B), Math.sin(C)*Math.sin(A), 0,
 Math.cos(B)*Math.sin(A), -Math.sin(B), Math.cos(B)*Math.cos(A), 0,
 h*Math.cos(B)*Math.sin(A), -h*Math.sin(B)+p, h*Math.cos(B)*Math.cos(A), 1]

 gl.uniformMatrix4fv(_Pmatrix, false, proj_matrix);
 gl.uniformMatrix4fv(_Vmatrix, false, view_matrix);
 gl.uniformMatrix4fv(_Mmatrix, false, mo_matrix);


 buf(win0); 
 buf(win1);
 buf(edge_right_upper);
 buf(surface_bottom); 
 
 }
draw();


 </script>
 </body>
</html>

Результат работы программы с рупором из 2-х окон, одного ребра и одной поверхности:

Контрольное задание: Добавьте код для определения других элементов рупорной антенны — заднего окна, 3-х ребер и 3-х граней.

Полезные ссылки:

  1. WebGL — Quick Guide
  2. Введение в WebGL
  3. Настройка буфера вершин и буфера индексов
  4. Объекты как ассоциативные массивы

 

Автор: Николай Свирневский