Эта статья будет основана на платформе, представленной в первой части этого мини-сериала, добавив импортер моделей и собственный класс для 3D-объектов. Вы также познакомитесь с анимацией и элементами управления. Нам предстоит многое пройти, так что давайте начнем!
Эта статья в значительной степени опирается на первую статью, поэтому, если вы еще не прочитали ее, вам следует начать с нее в первую очередь .
WebGL манипулирует элементами в трехмерном мире, используя математические формулы, известные как преобразования . Поэтому, прежде чем мы начнем создавать 3D-класс, я покажу вам некоторые виды преобразований и способы их реализации.
Трансформации
Есть три основных преобразования при работе с трехмерными объектами.
- перемещение
- пересчет
- вращающийся
Каждая из этих функций может быть выполнена по оси X, Y или Z, что дает в общей сложности девять базовых преобразований. Все они по-разному влияют на матрицу преобразования 4х4 трехмерного объекта. Чтобы выполнить несколько преобразований для одного и того же объекта без проблем с перекрытием, мы должны умножить преобразование в матрицу объекта и не применять его непосредственно к матрице объекта. Переезд проще всего, так что давайте начнем с него.
Переезд АКА «Перевод»
Перемещение трехмерного объекта — это одно из самых простых преобразований, которое вы можете сделать, потому что для него есть специальное место в матрице 4×4. Там нет необходимости в какой-либо математике; просто поместите координаты X, Y и Z в матрицу и все готово. Если вы смотрите на матрицу 4х4, то это первые три цифры в нижнем ряду. Кроме того, вы должны знать, что положительный Z находится позади камеры. Следовательно, значение Z, равное -100, помещает объект на 100 единиц внутрь на экране. Мы компенсируем это в нашем коде.
Чтобы выполнить несколько преобразований, вы не можете просто изменить реальную матрицу объекта; Вы должны применить преобразование к новой пустой матрице, известной как единичная матрица, и умножить ее на основную матрицу.
Умножение матриц может быть немного сложным для понимания, но основная идея заключается в том, что каждый вертикальный столбец умножается на горизонтальный ряд второй матрицы. Например, первое число будет первой строкой, умноженной на первый столбец другой матрицы. Второе число в новой матрице будет первой строкой, умноженной на второй столбец другой матрицы, и так далее.
Следующий фрагмент кода, который я написал для умножения двух матриц в JavaScript. Добавьте это в ваш файл .js
который вы создали в первой части этой серии:
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
|
function MH(A, B) {
var Sum = 0;
for (var i = 0; i < A.length; i++) {
Sum += A[i] * B[i];
}
return Sum;
}
function MultiplyMatrix(A, B) {
var A1 = [A[0], A[1], A[2], A[3]];
var A2 = [A[4], A[5], A[6], A[7]];
var A3 = [A[8], A[9], A[10], A[11]];
var A4 = [A[12], A[13], A[14], A[15]];
var B1 = [B[0], B[4], B[8], B[12]];
var B2 = [B[1], B[5], B[9], B[13]];
var B3 = [B[2], B[6], B[10], B[14]];
var B4 = [B[3], B[7], B[11], B[15]];
return [
MH(A1, B1), MH(A1, B2), MH(A1, B3), MH(A1, B4),
MH(A2, B1), MH(A2, B2), MH(A2, B3), MH(A2, B4),
MH(A3, B1), MH(A3, B2), MH(A3, B3), MH(A3, B4),
MH(A4, B1), MH(A4, B2), MH(A4, B3), MH(A4, B4)];
}
|
Я не думаю, что это требует какого-либо объяснения, поскольку это просто необходимая математика для умножения матриц. Давайте перейдем к масштабированию.
пересчет
Масштабирование модели также довольно просто — это простое умножение. Вы должны умножить первые три диагональных числа на любой масштаб. Еще раз, порядок — X, Y и Z. Итак, если вы хотите, чтобы ваш объект был увеличен в два раза по всем трем осям, вы умножили бы первый, шестой и одиннадцатый элементы в вашем массиве на 2.
вращающийся
Вращение — самое сложное преобразование, потому что для каждой из трех осей есть разные уравнения. На следующем рисунке показаны уравнения вращения для каждой оси:
Не волнуйтесь, если эта картина не имеет смысла для вас; мы скоро рассмотрим реализацию JavaScript.
Важно отметить, что имеет значение, в каком порядке вы выполняете преобразования; разные заказы дают разные результаты.
Важно отметить, что имеет значение, в каком порядке вы выполняете преобразования; разные заказы дают разные результаты. Если вы сначала переместите свой объект, а затем поверните его, WebGL будет качать ваш объект как летучая мышь, а не вращать объект на месте. Если сначала повернуть, а затем переместить объект, у вас будет объект в указанном месте, но он будет смотреть в указанном вами направлении. Это потому, что преобразования выполняются вокруг начальной точки — 0,0,0 — в трехмерном мире. Там нет правильного или неправильного порядка. Все зависит от эффекта, который вы ищете.
Для создания более сложной анимации может потребоваться более одного преобразования. Например, если вы хотите, чтобы дверь открывалась на петлях, вы должны переместить дверь так, чтобы ее петли были на оси Y (то есть 0 на оси X и Z). Затем вы вращаете ось Y, чтобы дверь вращалась на петлях. Наконец, вы бы снова переместили его в нужное место на вашей сцене.
Анимации такого типа немного более индивидуальны для каждой ситуации, поэтому я не собираюсь делать для них функцию. Я, однако, сделаю функцию с самым основным порядком: масштабирование, вращение, а затем перемещение. Это гарантирует, что все находится в указанном месте и обращено в правильном направлении.
Теперь, когда у вас есть общее представление о математике всего этого и о том, как работают анимации, давайте создадим тип данных JavaScript для хранения наших 3D-объектов.
GL Объекты
Помните из первой части этой серии, что вам нужно три массива, чтобы нарисовать базовый трехмерный объект: массив вершин, массив треугольников и массив текстур. Это будет основой нашего типа данных. Нам также нужны переменные для трех преобразований на каждой из трех осей. Наконец, нам нужны переменные для изображения текстуры и чтобы указать, закончилась ли загрузка модели.
Вот моя реализация трехмерного объекта в JavaScript:
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
|
function GLObject(VertexArr, TriangleArr, TextureArr, ImageSrc) {
this.Pos = {
X: 0,
Y: 0,
Z: 0
};
this.Scale = {
X: 1.0,
Y: 1.0,
Z: 1.0
};
this.Rotation = {
X: 0,
Y: 0,
Z: 0
};
this.Vertices = VertexArr;
this.Triangles = TriangleArr;
this.TriangleCount = TriangleArr.length;
this.TextureMap = TextureArr;
this.Image = new Image();
this.Image.onload = function () {
this.ReadyState = true;
};
this.Image.src = ImageSrc;
this.Ready = false;
//Add Transformation function Here
}
|
Я добавил две отдельные «готовые» переменные: одна для того, когда изображение готово, и одна для модели. Когда изображение будет готово, я подготовлю модель путем преобразования изображения в текстуру WebGL и буферизации трех массивов в буферах WebGL. Это ускорит наше приложение, в отличие от буферизации данных в каждом цикле отрисовки. Поскольку мы преобразуем массивы в буферы, нам нужно сохранить количество треугольников в отдельной переменной.
Теперь давайте добавим функцию, которая будет вычислять матрицу преобразования объекта. Эта функция возьмет все локальные переменные и умножит их в порядке, который я упоминал ранее (масштаб, вращение, а затем перевод). Вы можете поиграть с этим заказом для различных эффектов. Замените //Add Transformation function 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
this.GetTransforms = function () {
//Create a Blank Identity Matrix
var TMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
//Scaling
var Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
Temp[0] *= this.Scale.X;
Temp[5] *= this.Scale.Y;
Temp[10] *= this.Scale.Z;
TMatrix = MultiplyMatrix(TMatrix, Temp);
//Rotating X
Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
var X = this.Rotation.X * (Math.PI / 180.0);
Temp[5] = Math.cos(X);
Temp[6] = Math.sin(X);
Temp[9] = -1 * Math.sin(X);
Temp[10] = Math.cos(X);
TMatrix = MultiplyMatrix(TMatrix, Temp);
//Rotating Y
Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
var Y = this.Rotation.Y * (Math.PI / 180.0);
Temp[0] = Math.cos(Y);
Temp[2] = -1 * Math.sin(Y);
Temp[8] = Math.sin(Y);
Temp[10] = Math.cos(Y);
TMatrix = MultiplyMatrix(TMatrix, Temp);
//Rotating Z
Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
var Z = this.Rotation.Z * (Math.PI / 180.0);
Temp[0] = Math.cos(Z);
Temp[1] = Math.sin(Z);
Temp[4] = -1 * Math.sin(Z);
Temp[5] = Math.cos(Z);
TMatrix = MultiplyMatrix(TMatrix, Temp);
//Moving
Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
Temp[12] = this.Pos.X;
Temp[13] = this.Pos.Y;
Temp[14] = this.Pos.Z * -1;
return MultiplyMatrix(TMatrix, Temp);
}
|
Поскольку формулы вращения перекрывают друг друга, они должны выполняться по одному. Эта функция заменяет функцию MakeTransform
из последнего урока, поэтому вы можете удалить ее из своего скрипта.
OBJ Импортер
Теперь, когда у нас построен 3D-класс, нам нужен способ загрузки данных. Мы сделаем простой импортер моделей, который преобразует файлы .obj
в необходимые данные для создания одного из наших недавно созданных объектов GLObject
. Я использую формат модели .obj
, потому что он хранит все данные в необработанном виде и имеет очень хорошую документацию о том, как он хранит информацию. Если ваша программа 3D-моделирования не поддерживает экспорт в .obj
, то вы всегда можете создать импортер для какого-либо другого формата данных. .obj
— это стандартный тип файла 3D; так что это не должно быть проблемой. Кроме того, вы также можете скачать Blender, бесплатное кроссплатформенное приложение для 3D-моделирования, которое поддерживает экспорт в .obj
В файлах .obj
первые две буквы каждой строки указывают нам, какие данные содержит эта строка. « v
» для линии «координаты вершины», « vt
» для линии «координаты текстуры» и « f
» для линии отображения. С этой информацией я написал следующую функцию:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
function LoadModel(ModelName, CB) {
var Ajax = new XMLHttpRequest();
Ajax.onreadystatechange = function () {
if (Ajax.readyState == 4 && Ajax.status == 200) {
//Parse Model Data
var Script = Ajax.responseText.split(«\n»);
var Vertices = [];
var VerticeMap = [];
var Triangles = [];
var Textures = [];
var TextureMap = [];
var Normals = [];
var NormalMap = [];
var Counter = 0;
|
Эта функция принимает имя модели и функцию обратного вызова. Обратный вызов принимает четыре массива: вершину, треугольник, текстуру и нормальные массивы. Я еще не рассмотрел нормали, поэтому вы можете просто игнорировать их. Я рассмотрю их в следующей статье, когда мы обсудим освещение.
Импортер начинает с создания объекта XMLHttpRequest
и определения его onreadystatechange
события onreadystatechange
. Внутри обработчика мы разбиваем файл на строки и определяем несколько переменных. Файлы .obj
сначала определяют все уникальные координаты, а затем определяют их порядок. Вот почему есть две переменные для вершин, текстур и нормалей. Переменная counter используется для заполнения массива треугольников, поскольку файлы .obj
определяют треугольники по порядку.
Далее мы должны пройти через каждую строку файла и проверить, что это за строка:
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
|
for (var I in Script) {
var Line = Script[I];
//If Vertice Line
if (Line.substring(0, 2) == «v «) {
var Row = Line.substring(2).split(» «);
Vertices.push({
X: parseFloat(Row[0]),
Y: parseFloat(Row[1]),
Z: parseFloat(Row[2])
});
}
//Texture Line
else if (Line.substring(0, 2) == «vt») {
var Row = Line.substring(3).split(» «);
Textures.push({
X: parseFloat(Row[0]),
Y: parseFloat(Row[1])
});
}
//Normals Line
else if (Line.substring(0, 2) == «vn») {
var Row = Line.substring(3).split(» «);
Normals.push({
X: parseFloat(Row[0]),
Y: parseFloat(Row[1]),
Z: parseFloat(Row[2])
});
}
|
Первые три типа линий довольно просты; они содержат список уникальных координат для вершин, текстур и нормалей. Все, что нам нужно сделать, это вставить эти координаты в соответствующие им массивы. Последний тип строки немного сложнее, потому что он может содержать несколько вещей. Он может содержать только вершины, вершины и текстуры, вершины, текстуры и нормали. Таким образом, мы должны проверить для каждого из этих трех случаев. Следующий код делает это:
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
35
36
37
38
39
40
41
42
43
44
45
46
|
//Mapping Line
else if (Line.substring(0, 2) == «f «) {
var Row = Line.substring(2).split(» «);
for (var T in Row) {
//Remove Blank Entries
if (Row[T] != «») {
//If this is a multi-value entry
if (Row[T].indexOf(«/») != -1) {
//Split the different values
var TC = Row[T].split(«/»);
//Increment The Triangles Array
Triangles.push(Counter);
Counter++;
//Insert the Vertices
var index = parseInt(TC[0]) — 1;
VerticeMap.push(Vertices[index].X);
VerticeMap.push(Vertices[index].Y);
VerticeMap.push(Vertices[index].Z);
//Insert the Textures
index = parseInt(TC[1]) — 1;
TextureMap.push(Textures[index].X);
TextureMap.push(Textures[index].Y);
//If This Entry Has Normals Data
if (TC.length > 2) {
//Insert Normals
index = parseInt(TC[2]) — 1;
NormalMap.push(Normals[index].X);
NormalMap.push(Normals[index].Y);
NormalMap.push(Normals[index].Z);
}
}
//For rows with just vertices
else {
Triangles.push(Counter);
Counter++;
var index = parseInt(Row[T]) — 1;
VerticeMap.push(Vertices[index].X);
VerticeMap.push(Vertices[index].Y);
VerticeMap.push(Vertices[index].Z);
}
}
}
}
|
Этот код более длинный, чем сложный. Хотя я рассмотрел сценарий, в котором файл .obj
содержит только данные о вершинах, наша структура требует вершин и текстурных координат. Если файл .obj
содержит только данные вершин, вам придется вручную добавить в него данные координат текстуры.
Теперь давайте передадим массивы в функцию обратного вызова и LoadModel
функцию LoadModel
:
1
2
3
4
5
6
7
8
|
}
//Return The Arrays
CB(VerticeMap, Triangles, TextureMap, NormalMap);
}
}
Ajax.open(«GET», ModelName + «.obj», true);
Ajax.send();
}
|
То, на что вам следует обратить внимание, это то, что наша платформа WebGL довольно проста и рисует только модели, которые сделаны из треугольников. Возможно, вам придется редактировать свои 3D-модели соответственно. К счастью, большинство 3D-приложений имеют функцию или плагин для триангуляции ваших моделей для вас. Я сделал простую модель дома с моими базовыми навыками моделирования, и я включу ее в исходные файлы, которые вы сможете использовать, если хотите.
Теперь давайте Draw
функцию Draw
из последнего урока, чтобы включить наш новый тип данных 3D-объекта:
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
35
36
37
38
|
this.Draw = function (Model) {
if (Model.Image.ReadyState == true && Model.Ready == false) {
this.PrepareModel(Model);
}
if (Model.Ready) {
this.GL.bindBuffer(this.GL.ARRAY_BUFFER, Model.Vertices);
this.GL.vertexAttribPointer(this.VertexPosition, 3, this.GL.FLOAT, false, 0, 0);
this.GL.bindBuffer(this.GL.ARRAY_BUFFER, Model.TextureMap);
this.GL.vertexAttribPointer(this.VertexTexture, 2, this.GL.FLOAT, false, 0, 0);
this.GL.bindBuffer(this.GL.ELEMENT_ARRAY_BUFFER, Model.Triangles);
//Generate The Perspective Matrix
var PerspectiveMatrix = MakePerspective(45, this.AspectRatio, 1, 1000.0);
var TransformMatrix = Model.GetTransforms();
//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, Model.Image);
//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, Model.TriangleCount, this.GL.UNSIGNED_SHORT, 0);
}
};
|
Новая функция рисования сначала проверяет, была ли модель подготовлена для WebGL. Если текстура загружена, она подготовит модель для рисования. Мы PrepareModel
функции PrepareModel
через минуту. Если модель готова, она подключит свои буферы к шейдерам и загрузит матрицы перспективы и преобразования, как это было раньше. Единственное реальное отличие состоит в том, что теперь он берет все данные из модельного объекта.
Функция PrepareModel
просто преобразует текстуру и массивы данных в WebGL-совместимые переменные. Вот функция; добавьте его прямо перед функцией рисования:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
this.PrepareModel = function (Model) {
Model.Image = this.LoadTexture(Model.Image);
//Convert Arrays to buffers
var Buffer = this.GL.createBuffer();
this.GL.bindBuffer(this.GL.ARRAY_BUFFER, Buffer);
this.GL.bufferData(this.GL.ARRAY_BUFFER, new Float32Array(Model.Vertices), this.GL.STATIC_DRAW);
Model.Vertices = Buffer;
Buffer = this.GL.createBuffer();
this.GL.bindBuffer(this.GL.ELEMENT_ARRAY_BUFFER, Buffer);
this.GL.bufferData(this.GL.ELEMENT_ARRAY_BUFFER, new Uint16Array(Model.Triangles), this.GL.STATIC_DRAW);
Model.Triangles = Buffer;
Buffer = this.GL.createBuffer();
this.GL.bindBuffer(this.GL.ARRAY_BUFFER, Buffer);
this.GL.bufferData(this.GL.ARRAY_BUFFER, new Float32Array(Model.TextureMap), this.GL.STATIC_DRAW);
Model.TextureMap = Buffer;
Model.Ready = true;
};
|
Теперь наш фреймворк готов, и мы можем перейти к HTML-странице.
HTML-страница
Вы можете стереть все, что находится внутри тегов script
поскольку теперь мы можем писать код более кратко благодаря нашему новому GLObject
данных GLObject
.
Это полный JavaScript:
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 GL;
var Building;
function Ready() {
GL = new WebGL(«GLCanvas», «FragmentShader», «VertexShader»);
LoadModel(«House», function (VerticeMap, Triangles, TextureMap) {
Building = new GLObject(VerticeMap, Triangles, TextureMap, «House.png»);
Building.Pos.Z = 650;
//My Model Was a bit too big
Building.Scale.X = 0.5;
Building.Scale.Y = 0.5;
Building.Scale.Z = 0.5;
//And Backwards
Building.Rotation.Y = 180;
setInterval(Update, 33);
});
}
function Update() {
Building.Rotation.Y += 0.2
GL.Draw(Building);
}
|
Мы загружаем модель и просим страницу обновлять ее примерно тридцать раз в секунду. Функция Update
вращает модель по оси Y, что достигается обновлением свойства Y Rotation
объекта. Моя модель была слишком большой для сцены WebGL, и это было задом наперед, поэтому мне нужно было внести некоторые коррективы в код.
Если вы не делаете какую-то кинематографическую презентацию WebGL, вы, вероятно, захотите добавить некоторые элементы управления. Давайте посмотрим, как мы можем добавить некоторые элементы управления клавиатурой в наше приложение.
Управление с клавиатуры
На самом деле это не техника WebGL, а встроенная функция JavaScript, но она удобна для управления и позиционирования ваших 3D-моделей. Все, что вам нужно сделать, это добавить прослушиватель событий к событиям клавиатуры или нажатия клавиш и проверить, какая клавиша была нажата. Каждый ключ имеет специальный код, и хороший способ выяснить, какой код соответствует ключу, — записать коды ключей на консоль при возникновении события. Итак, перейдите в область, где я загрузил модель, и добавьте следующий код сразу после строки setInterval
:
1
|
document.onkeydown = handleKeyDown;
|
Это установит функцию handleKeyDown
для обработки события handleKeyDown
. Вот код для функции handleKeyDown
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
function handleKeyDown(event) {
//You can uncomment the next line to find out each key’s code
//alert(event.keyCode);
if (event.keyCode == 37) {
//Left Arrow Key
Building.Pos.X -= 4;
} else if (event.keyCode == 38) {
//Up Arrow Key
Building.Pos.Y += 4;
} else if (event.keyCode == 39) {
//Right Arrow Key
Building.Pos.X += 4;
} else if (event.keyCode == 40) {
//Down Arrow Key
Building.Pos.Y -= 4;
}
}
|
Все, что делает эта функция, это обновляет свойства объекта; платформа WebGL позаботится обо всем остальном.
Вывод
Мы еще не закончили! В третьей и последней части этого мини-сериала мы рассмотрим различные виды освещения и как связать все это с некоторыми 2D-вещами!
Спасибо за чтение, и, как всегда, если у вас есть какие-либо вопросы, не стесняйтесь оставлять комментарии ниже!