Этот пост является продолжением серии постов о WebGL. Первый начался с основ и предыдущий был о 2D матрицах. Если вы не читали их, пожалуйста, просмотрите их сначала.
В последнем посте мы рассмотрели, как работают 2D матрицы. Мы говорили о том, как трансляция, вращение, масштабирование и даже проекция из пикселей в пространство отсечения могут быть выполнены одной матрицей и магической матричной математикой. Чтобы сделать 3D, нужно только небольшой шаг оттуда.
В наших предыдущих 2D примерах у нас были 2D точки (x, y), которые мы умножали на матрицу 3x3. Чтобы сделать 3D, нам нужны 3D точки (x, y, z) и матрица 4x4.
Давайте возьмем наш последний пример и изменим его на 3D. Мы снова используем F, но на этот раз 3D ‘F’.
Первое, что нам нужно сделать, это изменить вершинный шейдер для обработки 3D. Вот старый вершинный шейдер.
#version 300 es
// атрибут - это вход (in) в вершинный шейдер.
// Он будет получать данные из буфера
in vec2 a_position;
// Матрица для преобразования позиций
uniform mat3 u_matrix;
// все шейдеры имеют основную функцию
void main() {
// Умножаем позицию на матрицу.
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
А вот новый
// атрибут - это вход (in) в вершинный шейдер.
// Он будет получать данные из буфера
*in vec4 a_position;
// Матрица для преобразования позиций
*uniform mat4 u_matrix;
// все шейдеры имеют основную функцию
void main() {
// Умножаем позицию на матрицу.
* gl_Position = u_matrix * a_position;
}
Он стал еще проще! Так же, как в 2D мы предоставляли x
и y
, а затем
устанавливали z
в 1, в 3D мы будем предоставлять x
, y
и z
, и нам нужно, чтобы w
был 1, но мы можем воспользоваться тем фактом, что для атрибутов
w
по умолчанию равен 1.
Затем нам нужно предоставить 3D данные.
...
// Говорим атрибуту, как получать данные из positionBuffer (ARRAY_BUFFER)
* var size = 3; // 3 компонента на итерацию
var type = gl.FLOAT; // данные - это 32-битные числа с плавающей точкой
var normalize = false; // не нормализуем данные
var stride = 0; // 0 = двигаемся вперед на size * sizeof(type) на каждой итерации, чтобы получить следующую позицию
var offset = 0; // начинаем с начала буфера
gl.vertexAttribPointer(
positionAttributeLocation, size, type, normalize, stride, offset);
...
// Заполняем текущий буфер ARRAY_BUFFER
// значениями, которые определяют букву 'F'.
function setGeometry(gl) {
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
// левая колонка
0, 0, 0,
30, 0, 0,
0, 150, 0,
0, 150, 0,
30, 0, 0,
30, 150, 0,
// верхняя перекладина
30, 0, 0,
100, 0, 0,
30, 30, 0,
30, 30, 0,
100, 0, 0,
100, 30, 0,
// средняя перекладина
30, 60, 0,
67, 60, 0,
30, 90, 0,
30, 90, 0,
67, 60, 0,
67, 90, 0]),
gl.STATIC_DRAW);
}
Затем нам нужно изменить все матричные функции с 2D на 3D
Вот 2D (до) версии m3.translation, m3.rotation и m3.scaling
var m3 = {
translation: function translation(tx, ty) {
return [
1, 0, 0,
0, 1, 0,
tx, ty, 1
];
},
rotation: function rotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c,-s, 0,
s, c, 0,
0, 0, 1
];
},
scaling: function scaling(sx, sy) {
return [
sx, 0, 0,
0, sy, 0,
0, 0, 1
];
},
};
А вот обновленные 3D версии.
var m4 = {
translation: function(tx, ty, tz) {
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
tx, ty, tz, 1,
];
},
xRotation: function(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1,
];
},
yRotation: function(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1,
];
},
zRotation: function(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
];
},
scaling: function(sx, sy, sz) {
return [
sx, 0, 0, 0,
0, sy, 0, 0,
0, 0, sz, 0,
0, 0, 0, 1,
];
},
};
Обратите внимание, что теперь у нас есть 3 функции вращения. Нам нужна была только одна в 2D, так как мы
эффективно вращались только вокруг оси Z. Теперь же, чтобы делать 3D, мы
также хотим иметь возможность вращаться вокруг оси X и оси Y. Вы
можете видеть, глядя на них, что они все очень похожи. Если бы мы
их вывели, вы бы увидели, что они упрощаются точно так же, как раньше
Вращение Z
<div class="webgl_center">
<div>newX = x * c + y * s;</div>
<div>newY = x * -s + y * c;</div>
</div>
Вращение Y
<div class="webgl_center">
<div>newX = x * c + z * s;</div>
<div>newZ = x * -s + z * c;</div>
</div>
Вращение X
<div class="webgl_center">
<div>newY = y * c + z * s;</div>
<div>newZ = y * -s + z * c;</div>
</div>
что дает вам эти вращения.
<iframe class="external_diagram" src="resources/axis-diagram.html" style="width: 540px; height: 240px;"></iframe>
Аналогично мы сделаем наши упрощенные функции
```js
translate: function(m, tx, ty, tz) {
return m4.multiply(m, m4.translation(tx, ty, tz));
},
xRotate: function(m, angleInRadians) {
return m4.multiply(m, m4.xRotation(angleInRadians));
},
yRotate: function(m, angleInRadians) {
return m4.multiply(m, m4.yRotation(angleInRadians));
},
zRotate: function(m, angleInRadians) {
return m4.multiply(m, m4.zRotation(angleInRadians));
},
scale: function(m, sx, sy, sz) {
return m4.multiply(m, m4.scaling(sx, sy, sz));
},
И нам нужна функция умножения матриц 4x4
multiply: function(a, b) {
var b00 = b[0 * 4 + 0];
var b01 = b[0 * 4 + 1];
var b02 = b[0 * 4 + 2];
var b03 = b[0 * 4 + 3];
var b10 = b[1 * 4 + 0];
var b11 = b[1 * 4 + 1];
var b12 = b[1 * 4 + 2];
var b13 = b[1 * 4 + 3];
var b20 = b[2 * 4 + 0];
var b21 = b[2 * 4 + 1];
var b22 = b[2 * 4 + 2];
var b23 = b[2 * 4 + 3];
var b30 = b[3 * 4 + 0];
var b31 = b[3 * 4 + 1];
var b32 = b[3 * 4 + 2];
var b33 = b[3 * 4 + 3];
var a00 = a[0 * 4 + 0];
var a01 = a[0 * 4 + 1];
var a02 = a[0 * 4 + 2];
var a03 = a[0 * 4 + 3];
var a10 = a[1 * 4 + 0];
var a11 = a[1 * 4 + 1];
var a12 = a[1 * 4 + 2];
var a13 = a[1 * 4 + 3];
var a20 = a[2 * 4 + 0];
var a21 = a[2 * 4 + 1];
var a22 = a[2 * 4 + 2];
var a23 = a[2 * 4 + 3];
var a30 = a[3 * 4 + 0];
var a31 = a[3 * 4 + 1];
var a32 = a[3 * 4 + 2];
var a33 = a[3 * 4 + 3];
return [
b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30,
b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31,
b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32,
b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33,
b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30,
b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31,
b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32,
b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33,
b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30,
b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31,
b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32,
b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33,
b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30,
b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31,
b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32,
b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33,
];
},
Нам также нужно обновить функцию проекции. Вот старая
projection: function (width, height) {
// Примечание: Эта матрица переворачивает ось Y, так что 0 находится сверху.
return [
2 / width, 0, 0,
0, -2 / height, 0,
-1, 1, 1
];
},
}
которая преобразовывала из пикселей в пространство отсечения. Для нашей первой попытки расширить её до 3D давайте попробуем
projection: function(width, height, depth) {
// Примечание: Эта матрица переворачивает ось Y, так что 0 находится сверху.
return [
2 / width, 0, 0, 0,
0, -2 / height, 0, 0,
0, 0, 2 / depth, 0,
-1, 1, 0, 1,
];
},
Так же, как нам нужно было преобразовать из пикселей в пространство отсечения для X и Y, для
Z нам нужно сделать то же самое. В этом случае я делаю ось Z также в единицах пикселей.
Я передам некоторое значение, аналогичное width
для depth
,
так что наше пространство будет от 0 до width
пикселей в ширину, от 0 до height
пикселей в высоту, но
для depth
это будет от -depth / 2
до +depth / 2
.
Наконец, нам нужно обновить код, который вычисляет матрицу.
// Вычисляем матрицу
* var matrix = m4.projection(gl.canvas.clientWidth, gl.canvas.clientHeight, 400);
* matrix = m4.translate(matrix, translation[0], translation[1], translation[2]);
* matrix = m4.xRotate(matrix, rotation[0]);
* matrix = m4.yRotate(matrix, rotation[1]);
* matrix = m4.zRotate(matrix, rotation[2]);
* matrix = m4.scale(matrix, scale[0], scale[1], scale[2]);
// Устанавливаем матрицу.
* gl.uniformMatrix4fv(matrixLocation, false, matrix);
И вот этот пример.
Первая проблема, которая у нас есть, заключается в том, что наша геометрия - это плоский F, что затрудняет видение любого 3D. Чтобы исправить это, давайте расширим геометрию до 3D. Наш текущий F состоит из 3 прямоугольников, по 2 треугольника каждый. Чтобы сделать его 3D, потребуется всего 16 прямоугольников. 3 прямоугольника спереди, 3 сзади, 1 слева, 4 справа, 2 сверху, 3 снизу.
Это довольно много, чтобы перечислить здесь. 16 прямоугольников с 2 треугольниками на прямоугольник и 3 вершинами на треугольник - это 96 вершин. Если вы хотите увидеть все из них, просмотрите исходный код примера.
Нам нужно рисовать больше вершин, поэтому
// Рисуем геометрию.
var primitiveType = gl.TRIANGLES;
var offset = 0;
* var count = 16 * 6;
gl.drawArrays(primitiveType, offset, count);
И вот эта версия
Перемещая ползунки, довольно трудно сказать, что это 3D. Давайте попробуем раскрасить каждый прямоугольник в разный цвет. Для этого мы добавим еще один атрибут к нашему вершинному шейдеру и varying для передачи его из вершинного шейдера в фрагментный шейдер.
Вот новый вершинный шейдер
#version 300 es
// атрибут - это вход (in) в вершинный шейдер.
// Он будет получать данные из буфера
in vec4 a_position;
+in vec4 a_color;
// Матрица для преобразования позиций
uniform mat4 u_matrix;
+// varying для передачи цвета в фрагментный шейдер
+out vec4 v_color;
// все шейдеры имеют основную функцию
void main() {
// Умножаем позицию на матрицу.
gl_Position = u_matrix * a_position;
+ // Передаем цвет в фрагментный шейдер.
+ v_color = a_color;
}
И нам нужно использовать этот цвет в фрагментном шейдере
#version 300 es
precision highp float;
+// varying цвет, переданный из вершинного шейдера
+in vec4 v_color;
// нам нужно объявить выход для фрагментного шейдера
out vec4 outColor;
void main() {
* outColor = v_color;
}
Нам нужно найти местоположение атрибута для предоставления цветов, затем настроить другой буфер и атрибут для предоставления цветов.
...
var colorAttributeLocation = gl.getAttribLocation(program, "a_color");
...
// создаем буфер цветов, делаем его текущим ARRAY_BUFFER
// и копируем значения цветов
var colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
setColors(gl);
// Включаем атрибут
gl.enableVertexAttribArray(colorAttributeLocation);
// Говорим атрибуту, как получать данные из colorBuffer (ARRAY_BUFFER)
var size = 3; // 3 компонента на итерацию
var type = gl.UNSIGNED_BYTE; // данные - это 8-битные беззнаковые байты
var normalize = true; // преобразуем из 0-255 в 0.0-1.0
var stride = 0; // 0 = двигаемся вперед на size * sizeof(type) на каждой
// итерации, чтобы получить следующий цвет
var offset = 0; // начинаем с начала буфера
gl.vertexAttribPointer(
colorAttributeLocation, size, type, normalize, stride, offset);
...
// Заполняем буфер цветами для 'F'.
function setColors(gl) {
gl.bufferData(
gl.ARRAY_BUFFER,
new Uint8Array([
// левая колонка спереди
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
// верхняя перекладина спереди
200, 70, 120,
200, 70, 120,
...
...
gl.STATIC_DRAW);
}
Теперь мы получаем это.
Ой, что это за беспорядок? Ну, оказывается, все различные части этого 3D ‘F’, передняя, задняя, боковые и т.д., рисуются в том порядке, в котором они появляются в наших геометрических данных. Это не дает нам вполне желаемых результатов, так как иногда те, что сзади, рисуются после тех, что спереди.
Красноватая часть - это передняя часть ‘F’, но поскольку это первая часть наших данных, она рисуется первой, а затем другие треугольники за ней рисуются после, покрывая её. Например, фиолетовая часть на самом деле задняя часть ‘F’. Она рисуется 2-й, потому что приходит 2-й в наших данных.
Треугольники в WebGL имеют концепцию лицевой и обратной стороны. По умолчанию лицевой треугольник имеет свои вершины в направлении против часовой стрелки. Обратный треугольник имеет свои вершины в направлении по часовой стрелке.
WebGL имеет возможность рисовать только лицевые или обратные треугольники. Мы можем включить эту функцию с помощью
gl.enable(gl.CULL_FACE);
Хорошо, поместим это в нашу функцию drawScene
. С этой
функцией включенной, WebGL по умолчанию “отсекает” обратные треугольники.
“Отсекание” в данном случае - это модное слово для “не рисования”.
Обратите внимание, что насколько WebGL обеспокоен, является ли треугольник идущим по часовой стрелке или против часовой стрелки, зависит от вершин этого треугольника в пространстве отсечения. Другими словами, WebGL выясняет, является ли треугольник лицевым или обратным ПОСЛЕ того, как вы применили математику к вершинам в вершинном шейдере. Это означает, например, что треугольник по часовой стрелке, который масштабируется по X на -1, становится треугольником против часовой стрелки, или треугольник по часовой стрелке, повернутый на 180 градусов, становится треугольником против часовой стрелки. Поскольку у нас была отключена CULL_FACE, мы можем видеть как треугольники по часовой стрелке (лицевые), так и против часовой стрелки (обратные). Теперь, когда мы включили её, всякий раз, когда лицевой треугольник переворачивается либо из-за масштабирования, либо вращения, либо по какой-либо другой причине, WebGL не будет его рисовать. Это хорошая вещь, поскольку когда вы поворачиваете что-то в 3D, вы обычно хотите, чтобы любые треугольники, обращенные к вам, считались лицевыми.
С включенной CULL_FACE мы получаем это
Эй! Куда делись все треугольники? Оказывается, многие из них смотрят в неправильную сторону. Поверните его, и вы увидите, как они появляются, когда вы смотрите с другой стороны. К счастью, это легко исправить. Мы просто смотрим на те, которые обращены назад, и меняем местами 2 их вершины. Например, если один обратный треугольник
1, 2, 3,
40, 50, 60,
700, 800, 900,
мы просто меняем местами последние 2 вершины, чтобы сделать его лицевым.
1, 2, 3,
* 700, 800, 900,
* 40, 50, 60,
Проходя и исправляя все обратные треугольники, мы получаем это
Это ближе, но все еще есть еще одна проблема. Даже со всеми треугольниками, обращенными в правильном направлении, и с отсечением обратных, у нас все еще есть места, где треугольники, которые должны быть сзади, рисуются поверх треугольников, которые должны быть спереди.
Введите БУФЕР ГЛУБИНЫ.
Буфер глубины, иногда называемый Z-буфером, - это прямоугольник пикселей глубины, один пиксель глубины для каждого цветного пикселя, используемого для создания изображения. Когда WebGL рисует каждый цветной пиксель, он также может рисовать пиксель глубины. Он делает это на основе значений, которые мы возвращаем из вершинного шейдера для Z. Так же, как мы должны были преобразовать в пространство отсечения для X и Y, Z также находится в пространстве отсечения (от -1 до +1). Это значение затем преобразуется в значение пространства глубины (от 0 до +1). Перед тем как WebGL нарисует цветной пиксель, он проверит соответствующий пиксель глубины. Если значение глубины для пикселя, который он собирается нарисовать, больше значения соответствующего пикселя глубины, то WebGL не рисует новый цветной пиксель. В противном случае он рисует как новый цветной пиксель с цветом из вашего фрагментного шейдера, ТАК И новый пиксель глубины с новым значением глубины. Это означает, что пиксели, которые находятся за другими пикселями, не будут нарисованы.
Мы можем включить эту функцию почти так же просто, как мы включили отсечение с помощью
gl.enable(gl.DEPTH_TEST);
Нам также нужно очистить буфер глубины обратно до 1.0 перед тем, как мы начнем рисовать.
// Рисуем сцену.
function drawScene() {
...
// Очищаем холст И буфер глубины.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
...
И теперь мы получаем
что является 3D!
Одна небольшая вещь. В большинстве 3D математических библиотек нет функции projection
для
выполнения наших преобразований из пространства отсечения в пространство пикселей. Скорее обычно есть функция
называемая ortho
или orthographic
, которая выглядит так
var m4 = {
orthographic: function(left, right, bottom, top, near, far) {
return [
2 / (right - left), 0, 0, 0,
0, 2 / (top - bottom), 0, 0,
0, 0, 2 / (near - far), 0,
(left + right) / (left - right),
(bottom + top) / (bottom - top),
(near + far) / (near - far),
1,
];
}
В отличие от нашей упрощенной функции projection
выше, которая имела только параметры width, height и depth,
эта более распространенная ортографическая функция проекции позволяет нам передать left, right,
bottom, top, near и far, что дает нам больше гибкости. Чтобы использовать её так же, как
нашу оригинальную функцию проекции, мы бы вызвали её с
var left = 0;
var right = gl.canvas.clientWidth;
var bottom = gl.canvas.clientHeight;
var top = 0;
var near = 200;
var far = -200;
m4.orthographic(left, right, bottom, top, near, far);
В следующем посте я расскажу о том, как сделать перспективу.
```