Spec-Zone.ru › Web APIs

WebGL модель, представление и проекция

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

Примечание: Эта статья также доступна в виде набора контента MDN. Она также использует набор функций-утилит, доступных в глобальном объекте MDN.

Матрицы модели, представления и проекции

Индивидуальные преобразования точек и полигонов в пространстве в WebGL обрабатываются с помощью основных матриц преобразования, таких как перенос, масштабирование и поворот. Эти матрицы могут быть объединены и сгруппированы особым образом, чтобы сделать их полезными для отображения сложных 3D сцен. Эти составленные матрицы в конечном итоге перемещают исходные данные модели в специальное координатное пространство, называемое пространством обрезки. Это куб шириной 2 единицы, центрированный в (0,0,0) и с вершинами, которые варьируются от (-1,-1,-1) до (1,1,1). Это пространство обрезки сжимается до 2D-пространства и растрируется в изображение.

Первая матрица, обсуждаемая ниже, — это матрица модели, которая определяет, как вы берете исходные данные модели и перемещаете их в 3D-пространстве. Матрица проекции используется для преобразования координат пространства мира в координаты пространства обрезки. Часто используемая матрица проекции, матрица проекции с перспективой, используется для имитации эффекта обычной камеры, выступающей в роли наблюдателя в 3D виртуальном мире. Матрица представления отвечает за перемещение объектов в сцене для имитации изменения позиции камеры, изменяя то, что зритель в данный момент видит.

Разделы ниже предоставляют подробный обзор идей и реализации матриц модели, представления и проекции. Эти матрицы являются основополагающими для перемещения данных на экране и являются концепциями, которые выходят за рамки отдельных фреймворков и движков.

Пространство обрезки

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

A 3d graph showing clip space in WebGL.

На приведенном графике показано визуализация пространства обрезки, в котором должны размещаться все точки. Это куб со стороной две единицы, с одной вершиной в (-1,-1,-1) и противоположной вершиной в (1,1,1). Центр куба находится в точке (0,0,0). Эта восьмикубитовая система координат, используемая в пространстве обрезки, известна как нормализованные координаты устройства (NDC). Вы можете время от времени встречаться с этим термином при исследовании и работе с кодом WebGL.

В этом разделе мы непосредственно поместим наши данные в систему координат пространства обрезки. Обычно используются данные модели, находящиеся в какой-либо произвольной системе координат, и затем преобразуются с помощью матрицы, преобразующей координаты модели в систему координат пространства обрезки. В этом примере проще всего продемонстрировать работу пространства обрезки, используя значения координат модели, изменяющиеся от (-1,-1,-1) до (1,1,1). Нижеприведенный код создаст 2 треугольника, которые нарисуют квадрат на экране. Глубина Z в квадратах определяет, что отображается сверху, когда квадраты занимают одно и то же место. Более маленькие значения Z отображаются поверх более крупных значений Z.

Пример WebGLBox

В этом примере будет создан пользовательский объект WebGLBox, который нарисует 2D-прямоугольник на экране.

Примечание: Код для каждого примера WebGLBox доступен в этом репозитории GitHub и организован по разделам. Кроме того, ссылка на JSFiddle находится внизу каждого раздела.

Конструктор WebGLBox

Конструктор выглядит так:

function WebGLBox() {
  // Setup the canvas and WebGL context
  this.canvas = document.getElementById("canvas");
  this.canvas.width = window.innerWidth;
  this.canvas.height = window.innerHeight;
  this.gl = MDN.createContext(canvas);

  const gl = this.gl;

  // Setup a WebGL program, anything part of the MDN object is defined outside of this article
  this.webglProgram = MDN.createWebGLProgramFromIds(
    gl,
    "vertex-shader",
    "fragment-shader",
  );
  gl.useProgram(this.webglProgram);

  // Save the attribute and uniform locations
  this.positionLocation = gl.getAttribLocation(this.webglProgram, "position");
  this.colorLocation = gl.getUniformLocation(this.webglProgram, "color");

  // Tell WebGL to test the depth when drawing, so if a square is behind
  // another square it won't be drawn
  gl.enable(gl.DEPTH_TEST);
}

Отображение WebGLBox

Теперь мы создадим метод для отображения прямоугольника на экране.

WebGLBox.prototype.draw = function (settings) {
  // Create some attribute data; these are the triangles that will end being
  // drawn to the screen. There are two that form a square.

  const data = new Float32Array([
    //Triangle 1
    settings.left,
    settings.bottom,
    settings.depth,
    settings.right,
    settings.bottom,
    settings.depth,
    settings.left,
    settings.top,
    settings.depth,

    //Triangle 2
    settings.left,
    settings.top,
    settings.depth,
    settings.right,
    settings.bottom,
    settings.depth,
    settings.right,
    settings.top,
    settings.depth,
  ]);

  // Use WebGL to draw this onto the screen.

  // Performance Note: Creating a new array buffer for every draw call is slow.
  // This function is for illustration purposes only.

  const gl = this.gl;

  // Create a buffer and bind the data
  const buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

  // Setup the pointer to our attribute data (the triangles)
  gl.enableVertexAttribArray(this.positionLocation);
  gl.vertexAttribPointer(this.positionLocation, 3, gl.FLOAT, false, 0, 0);

  // Setup the color uniform that will be shared across all triangles
  gl.uniform4fv(this.colorLocation, settings.color);

  // Draw the triangles to the screen
  gl.drawArrays(gl.TRIANGLES, 0, 6);
};

Шейдеры — это фрагменты кода, написанные на языке GLSL, которые принимают наши точки данных и в конечном итоге выводят их на экран. Для удобства эти шейдеры хранятся в элементе <script>, который добавляется в программу с помощью пользовательской функции MDN.createWebGLProgramFromIds(). Эта функция является частью набора функций-утилит, написанных для этих учебных пособий, и подробно здесь не объясняется. Эта функция выполняет основные действия по загрузке исходного кода GLSL и его компиляции в программу WebGL. Функция принимает три параметра: контекст для отображения программы, идентификатор элемента <script>, содержащего шейдер вершин, и идентификатор элемента <script>, содержащего шейдер фрагментов. Шейдер вершин позиционирует вершины, а шейдер фрагментов присваивает цвет каждому пикселю.

Сначала посмотрите на шейдер вершин, который будет перемещать вершины на экране:

// The individual position vertex
attribute vec3 position;

void main() {
  // the gl_Position is the final position in clip space after the vertex shader modifies it
  gl_Position = vec4(position, 1.0);
}

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

precision mediump float;
uniform vec4 color;

void main() {
  gl_FragColor = color;
}

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

const box = new WebGLBox();

Сначала отобразим красный прямоугольник посередине.

box.draw({
  top: 0.5, // x
  bottom: -0.5, // x
  left: -0.5, // y
  right: 0.5, // y

  depth: 0, // z
  color: [1, 0.4, 0.4, 1], // red
});

Затем отобразим зелёный прямоугольник сверху и позади красного.

box.draw({
  top: 0.9, // x
  bottom: 0, // x
  left: -0.9, // y
  right: 0.9, // y

  depth: 0.5, // z
  color: [0.4, 1, 0.4, 1], // green
});

Наконец, для демонстрации того, что обрезка действительно происходит, этот прямоугольник не отображается, потому что он полностью находится вне пространства обрезки. Глубина находится за пределами диапазона от -1,0 до 1,0.

box.draw({
  top: 1, // x
  bottom: -1, // x
  left: -1, // y
  right: 1, // y

  depth: -1.5, // z
  color: [0.4, 0.4, 1, 1], // blue
});

Результаты

Открыть в JSFiddle

The results of drawing to clip space using WebGL.

Упражнение

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

Однородные координаты

Основная строка предыдущего фрагмента вершинного шейдера содержала этот код:

gl_Position = vec4(position, 1.0);

Переменная position была определена в методе draw() и передана в шейдер в качестве атрибута. Это трёхмерная точка, но переменная gl_Position, которая в конечном итоге передаётся по конвейеру, на самом деле четырёхмерная — вместо (x, y, z) она является (x, y, z, w). После z нет буквы, поэтому по соглашению эта четвёртая размерность обозначается как w. В приведённом примере координата w установлена в 1.0.

Очевидный вопрос: «Зачем дополнительная размерность?» Оказывается, это добавление позволяет использовать множество полезных техник для манипулирования трёхмерными данными. Эта дополнительная размерность вводит понятие перспективы в систему координат; с её помощью мы можем отобразить трёхмерные координаты в двумерное пространство — тем самым позволяя двум параллельным линиям пересекаться, когда они удаляются вдаль. Значение w используется в качестве делителя для других компонентов координаты, так что истинные значения x, y и z вычисляются как x/w, y/w и z/w (и w затем также w/w, становясь равным 1).

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

function cartesianToHomogeneous(point) {
  let x = point[0];
  let y = point[1];
  let z = point[2];

  return [x, y, z, 1];
}

function homogeneousToCartesian(point) {
  let x = point[0];
  let y = point[1];
  let z = point[2];
  let w = point[3];

  return [x / w, y / w, z / w];
}

Как уже упоминалось и показано в функциях выше, компонент w делит компоненты x, y и z. Когда компонент w является ненулевым вещественным числом, однородные координаты легко переводятся обратно в обычную точку в декартовом пространстве. А что происходит, если компонент w равен нулю? В JavaScript возвращаемое значение будет следующим.

homogeneousToCartesian([10, 4, 5, 0]);

Это вычисляется как: [Infinity, Infinity, Infinity].

Эти однородные координаты представляют некоторую точку в бесконечности. Это удобный способ представить луч, исходящий от начала координат в определённом направлении. Помимо луча, это можно рассматривать и как представление направленного вектора. Если эти однородные координаты умножаются на матрицу со сдвигом, то сдвиг эффективно удаляется.

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

Окончательное преимущество использования однородных координат заключается в том, что они отлично подходят для умножения на 4x4 матрицы. Вершина должна соответствовать хотя бы одной из размерностей матрицы, чтобы её можно было умножить на неё. 4x4 матрица может использоваться для кодирования различных полезных преобразований. Фактически, типичная матрица перспективного проектирования использует деление на компонент w для достижения своего преобразования.

Обрезка точек и полигонов из пространства обрезки происходит до того, как однородные координаты были преобразованы обратно в декартовы координаты (путём деления на w). Это конечное пространство известно как нормализованные координаты устройства или НКУ.

Чтобы начать работать с этой идеей, предыдущий пример можно изменить, чтобы разрешить использование компонента w.

//Redefine the triangles to use the W component
const data = new Float32Array([
  //Triangle 1
  settings.left,
  settings.bottom,
  settings.depth,
  settings.w,
  settings.right,
  settings.bottom,
  settings.depth,
  settings.w,
  settings.left,
  settings.top,
  settings.depth,
  settings.w,

  //Triangle 2
  settings.left,
  settings.top,
  settings.depth,
  settings.w,
  settings.right,
  settings.bottom,
  settings.depth,
  settings.w,
  settings.right,
  settings.top,
  settings.depth,
  settings.w,
]);

Затем вершинный шейдер использует четырёхмерную точку, переданную в качестве входных данных.

attribute vec4 position;

void main() {
  gl_Position = position;
}

Сначала мы рисуем красную коробку посередине, но устанавливаем W в 0,7. Поскольку координаты делятся на 0,7, они все будут увеличены.

box.draw({
  top: 0.5, // y
  bottom: -0.5, // y
  left: -0.5, // x
  right: 0.5, // x
  w: 0.7, // w - enlarge this box

  depth: 0, // z
  color: [1, 0.4, 0.4, 1], // red
});

Теперь мы рисуем зеленую коробку сверху, но уменьшаем её, установив компонент w в 1,1

box.draw({
  top: 0.9, // y
  bottom: 0, // y
  left: -0.9, // x
  right: 0.9, // x
  w: 1.1, // w - shrink this box

  depth: 0.5, // z
  color: [0.4, 1, 0.4, 1], // green
});

Эта последняя коробка не отображается, потому что она находится за пределами пространства обрезки. Глубина находится за пределами диапазона от -1,0 до 1,0.

box.draw({
  top: 1, // y
  bottom: -1, // y
  left: -1, // x
  right: 1, // x
  w: 1.5, // w - Bring this box into range

  depth: -1.5, // z
  color: [0.4, 0.4, 1, 1], // blue
});

Результаты

The results of using homogeneous coordinates to move the boxes around in WebGL.

Упражнения

  • Поэкспериментируйте с этими значениями, чтобы увидеть, как это влияет на то, что отображается на экране. Обратите внимание, как ранее обрезанная синяя коробка возвращается в диапазон, если установить её компонент w.
  • Попробуйте создать новую коробку, которая находится за пределами пространства обрезки, и верните её в него, разделив на w.

Преобразование модели

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

Наконец, вычисляется и устанавливается единственная матрица модели. Эта матрица представляет преобразования, которые должны выполняться над каждой точкой, образующей модель, для перемещения её в нужное пространство и выполнения других необходимых преобразований над каждой точкой модели. Это относится не только к каждой вершине, но и ко всем точкам каждой поверхности модели.

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

Следующий фрагмент кода определяет метод объекта CubeDemo, который создаст матрицу модели. Он использует пользовательские функции для создания и умножения матриц, как определено в MDN WebGL общем коде. Новая функция выглядит так:

CubeDemo.prototype.computeModelMatrix = function (now) {
  //Scale down by 50%
  const scale = MDN.scaleMatrix(0.5, 0.5, 0.5);

  // Rotate around X according to time
  const rotateX = MDN.rotateXMatrix(now * 0.0003);

  // Rotate around Y according to time slightly faster
  const rotateY = MDN.rotateYMatrix(now * 0.0005);

  // Move slightly down
  const position = MDN.translateMatrix(0, -0.1, 0);

  // Multiply together, make sure and read them in opposite order
  this.transforms.model = MDN.multiplyArrayOfMatrices([
    position, // step 4
    rotateY, // step 3
    rotateX, // step 2
    scale, // step 1
  ]);
};

Для использования этого в шейдере он должен быть установлен в униформную локацию. Локации для униформ сохраняются в объекте locations, показанном ниже:

this.locations.model = gl.getUniformLocation(webglProgram, "model");

И, наконец, униформ устанавливается в эту локацию. Это передаёт матрицу на графический процессор.

gl.uniformMatrix4fv(
  this.locations.model,
  false,
  new Float32Array(this.transforms.model),
);

В шейдере каждая позиционная вершина сначала преобразуется в однородные координаты (объект vec4) и затем умножается на матрицу модели.

gl_Position = model * vec4(position, 1.0);

Примечание: В JavaScript умножение матриц требует пользовательской функции, в то время как в шейдере оно встроено в язык с помощью простого оператора *.

Результаты

Просмотреть на JSFiddle

Using a model matrix

В этот момент значение w преобразованной точки по-прежнему равно 1,0. У куба всё ещё нет перспективы. В следующем разделе мы возьмём эту настройку и изменим значения w, чтобы добавить перспективу.

Упражнения

  • Уменьшите размер коробки с помощью матрицы масштабирования и поместите её в различные места в пространстве обрезки.
  • Попробуйте вывести её за пределы пространства обрезки.
  • Измените размер окна и понаблюдайте, как коробка искажается.
  • Добавьте матрицу rotateZ.

Деление на W

Легкий способ начать получение перспективы для нашей модели куба — скопировать координату Z в координату w. Обычно при преобразовании декартовой точки в однородные координаты она становится (x,y,z,1), но мы собираемся установить её на что-то вроде (x,y,z,z). На самом деле мы хотим убедиться, что z больше 0 для точек в поле зрения, поэтому немного изменим его, изменив значение на ((1.0 + z) * scaleFactor). Это переместит точку, которая обычно находится в пространстве обрезки (-1 до 1), в пространство, более похожее на (0 до 1), в зависимости от того, что установлено в масштабный коэффициент. Масштабный коэффициент изменяет окончательное значение w, делая его в целом выше или ниже.

Код шейдера выглядит следующим образом.

// First transform the point
vec4 transformedPosition = model * vec4(position, 1.0);

// How much effect does the perspective have?
float scaleFactor = 0.5;

// Set w by taking the z value which is typically ranged -1 to 1, then scale
// it to be from 0 to some number, in this case 0-1.
float w = (1.0 + transformedPosition.z) * scaleFactor;

// Save the new gl_Position with the custom w component
gl_Position = vec4(transformedPosition.xyz, w);

Результаты

Просмотреть на JSFiddle

Filling the W component and creating some projection.

Видите небольшой тёмно-синий треугольник? Это дополнительная грань, добавленная к нашему объекту, потому что поворот нашей фигуры вызвал выход этого угла за пределы пространства обрезки, что привело к его обрезке. Обратитесь к разделу Матрица перспективного проектирования ниже для введения в использование более сложных матриц для управления и предотвращения обрезки.

Упражнение

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

В следующем разделе мы перейдём от копирования Z в слот w к преобразованию его в матрицу.

Простое проектирование

Последний шаг заполнения компоненты w может быть выполнен с помощью простой матрицы. Начните с единичной матрицы:

const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];

MDN.multiplyPoint(identity, [2, 3, 4, 1]);
//> [2, 3, 4, 1]

Затем переместите единицу в последней колонке на один столбец вверх.

const copyZ = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0];

MDN.multiplyPoint(copyZ, [2, 3, 4, 1]);
//> [2, 3, 4, 4]

Однако в последнем примере мы выполнили (z + 1) * scaleFactor:

const scaleFactor = 0.5;

const simpleProjection = [
  1,
  0,
  0,
  0,
  0,
  1,
  0,
  0,
  0,
  0,
  1,
  scaleFactor,
  0,
  0,
  0,
  scaleFactor,
];

MDN.multiplyPoint(simpleProjection, [2, 3, 4, 1]);
//> [2, 3, 4, 2.5]

Более подробно можно увидеть, как это работает:

let x = 2 * 1 + 3 * 0 + 4 * 0 + 1 * 0;
let y = 2 * 0 + 3 * 1 + 4 * 0 + 1 * 0;
let z = 2 * 0 + 3 * 0 + 4 * 1 + 1 * 0;
let w = 2 * 0 + 3 * 0 + 4 * scaleFactor + 1 * scaleFactor;

Последнюю строку можно упростить до:

w = 4 * scaleFactor + 1 * scaleFactor;

Затем, вынося scaleFactor за скобки, получаем:

w = (4 + 1) * scaleFactor;

Что точно соответствует (z + 1) * scaleFactor из предыдущего примера.

В демонстрации с кубом добавлен дополнительный computeSimpleProjectionMatrix() метод. Он вызывается в draw() методе и получает в качестве параметра scaleFactor. Результат должен быть идентичен последнему примеру:

CubeDemo.prototype.computeSimpleProjectionMatrix = function (scaleFactor) {
  this.transforms.projection = [
    1,
    0,
    0,
    0,
    0,
    1,
    0,
    0,
    0,
    0,
    1,
    scaleFactor,
    0,
    0,
    0,
    scaleFactor,
  ];
};

Хотя результат идентичен, важным шагом здесь является обработка в вершинном шейдере. Вместо непосредственного изменения вершины, она умножается на дополнительную проекционную матрицу, которая (как следует из названия) проектирует 3D точки на 2D поверхность отрисовки:

// Make sure to read the transformations in reverse order
gl_Position = projection * model * vec4(position, 1.0);

Результаты

Просмотр на JSFiddle

A simple projection matrix

Область видимости

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

Во время рендеринга нам нужно определить, какие полигоны необходимо рендерить для представления сцены. Именно это определяет область видимости. Но что такое фрустум?

Фрустум — это 3D тело, которое получается при вырезании двух сечений из любого тела с помощью двух параллельных плоскостей. Рассмотрим нашу камеру, которая просматривает область, начинающуюся сразу перед её объективом и простирающуюся вдаль. Область обзора представляет собой четырёхугольную пирамиду с вершиной в объективе, четырьмя сторонами, соответствующими границам поля зрения, и основанием на самом удалённом видимом расстоянии, подобно этому:

A depiction of the entire viewing area of a camera. This area is a four-sided pyramid with its peak at the lens and its base at the world's maximum viewable distance.

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

Таким образом, первым шагом в сокращении количества полигонов, которые нам нужно вычислить и рендерить, мы преобразуем эту пирамиду в область видимости. Две плоскости, которые мы будем использовать для удаления вершин, чтобы сократить количество полигонов, — это ближняя плоскость обрезания и дальняя плоскость обрезания.

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

A depiction of the camera's view frustum; the near and far planes have removed part of the volume, reducing the polygon count.

Набор объектов, которые должны быть рендерены для каждого кадра, по сути, создаётся путём начала с набора всех объектов в сцене. Затем любые объекты, которые полностью находятся за пределами области видимости, удаляются из набора. Далее, объекты, частично выходящие за пределы области видимости, обрезаются путём удаления любых полигонов, которые полностью находятся за пределами области видимости, и обрезания полигонов, пересекающих границы области видимости, чтобы они больше не выходили за неё.

После этого у нас есть наибольший набор полигонов, которые полностью находятся в пределах области видимости. Этот список обычно дополнительно сокращается с помощью таких процессов, как обрезка задних граней (удаление полигонов, обращённых к камере тыльной стороной) и обрезка скрытых объектов с использованием определения скрытых поверхностей (удаление полигонов, которые не видны, потому что они полностью перекрываются полигонами, более близкими к объективу).

Матрица проекции перспективы

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

Матрица проекции перспективы — это тип проекционной матрицы, которая выполняет все эти требования. Математика также становится немного более сложной, и в этих примерах она не будет полностью объяснены. Короче говоря, она объединяет деление на w (как в предыдущих примерах) с некоторыми остроумными манипуляциями, основанными на подобных треугольниках. Если вы хотите прочитать полное объяснение математики, стоящей за ней, ознакомьтесь со следующими ссылками:

  • Матрица проекции OpenGL
  • Перспективная проекция
  • Попытка понять математику, стоящую за матрицей перспективы в WebGL

Важно отметить, что матрица проекции перспективы, используемая ниже, переворачивает ось z. В пространстве отсечения z+ отходит от наблюдателя, а с этой матрицей он направлен к наблюдателю.

Причина переворота оси z заключается в том, что система координат пространства отсечения является левой системой координат (в которой ось z направлена от наблюдателя и в экран), в то время как в математике, физике и 3D моделировании, а также для системы координат взгляда/глаза в OpenGL используется правая система координат (ось z направлена из экрана к наблюдателю). Подробнее об этом в соответствующих статьях Википедии: Система декартовых координат, Правило правой руки.

Давайте рассмотрим perspectiveMatrix() функцию, вычисляющую матрицу проекции перспективы.

MDN.perspectiveMatrix = function (
  fieldOfViewInRadians,
  aspectRatio,
  near,
  far,
) {
  const f = 1.0 / Math.tan(fieldOfViewInRadians / 2);
  const rangeInv = 1 / (near - far);

  return [
    f / aspectRatio,
    0,
    0,
    0,
    0,
    f,
    0,
    0,
    0,
    0,
    (near + far) * rangeInv,
    -1,
    0,
    0,
    near * far * rangeInv * 2,
    0,
  ];
};

Четыре параметра этой функции:

fieldOfViewInRadians

Угол в радианах, указывающий, сколько сцены видно наблюдателю сразу. Чем больше число, тем больше видно камерой. Геометрия на краях становится всё более искажённой, что эквивалентно широкоугольному объективу. При большем поле зрения объекты, как правило, становятся меньше. При меньшем поле зрения камера видит меньше и меньше в сцене. Объекты искажаются перспективной проекцией меньше, и объекты кажутся ближе к камере.

aspectRatio

Соотношение сторон сцены, которое равно ширине, делённой на высоту. В этих примерах это ширина окна, делённая на высоту окна. Введение этого параметра наконец решает проблему искажения модели при изменении размера и формы холста.

nearClippingPlaneDistance

Положительное число, указывающее расстояние в экран до плоскости, перпендикулярной полу, ближе к которой всё обрезается. Это отображается как -1 в пространстве отсечения и не должно быть равно 0.

farClippingPlaneDistance

Положительное число, указывающее расстояние до плоскости, за пределами которой геометрия обрезается. Это отображается как 1 в пространстве отсечения. Это значение должно быть разумно близко к расстоянию до геометрии, чтобы избежать ошибок точности при рендеринге.

В последней версии демонстрации с кубом метод computeSimpleProjectionMatrix() был заменён на метод computePerspectiveMatrix().

CubeDemo.prototype.computePerspectiveMatrix = function () {
  const fieldOfViewInRadians = Math.PI * 0.5;
  const aspectRatio = window.innerWidth / window.innerHeight;
  const nearClippingPlaneDistance = 1;
  const farClippingPlaneDistance = 50;

  this.transforms.projection = MDN.perspectiveMatrix(
    fieldOfViewInRadians,
    aspectRatio,
    nearClippingPlaneDistance,
    farClippingPlaneDistance,
  );
};

Код шейдера идентичен предыдущему примеру:

gl_Position = projection * model * vec4(position, 1.0);

Кроме того (не показано), матрицы позиции и масштаба модели были изменены, чтобы вывести их из пространства отсечения и в более обширную систему координат.

Результаты

Просмотр на JSFiddle

A true perspective matrix

Упражнения

  • Экспериментируйте с параметрами матрицы проекции перспективы и матрицы модели.
  • Замените матрицу проекции перспективы на матрицу ортогональной проекции. В общем коде WebGL MDN вы найдёте MDN.orthographicMatrix(). Это может заменить функцию MDN.perspectiveMatrix() в CubeDemo.prototype.computePerspectiveMatrix().

Матрица вида

Хотя некоторые графические библиотеки имеют виртуальную камеру, которую можно позиционировать и настраивать при построении сцены, OpenGL (и, следовательно, WebGL) этого не делает. Именно здесь появляется матрица вида. Её задача — перевести, повернуть и масштабировать объекты в сцене, чтобы они находились в нужном месте относительно наблюдателя, учитывая положение и ориентацию наблюдателя.

Имитация камеры

Это использует один из фундаментальных аспектов теории специальной относительности Эйнштейна: принцип систем отсчёта и относительного движения гласит, что с точки зрения наблюдателя вы можете смоделировать изменение положения и ориентации наблюдателя, применив противоположное изменение к объектам в сцене. В любом случае, результат выглядит одинаковым для наблюдателя.

Представьте себе коробку, стоящую на столе, и камеру, стоящую на столе в метре от коробки, нацеленную на неё, передняя часть которой направлена к камере. Затем представьте, как вы перемещаете камеру от коробки до двух метров (добавив метр к её Z-позиции), а затем сдвигаете её на 10 сантиметров влево. Коробка удаляется от камеры на это расстояние и немного сдвигается вправо, тем самым, выглядя меньше для камеры и немного открывая её левую сторону для камеры.

Теперь давайте сбросим сцену, вернув коробку в исходную точку, с камерой в двух метрах от неё, и прямо на неё направленной. Однако на этот раз камера зафиксирована на столе и не может быть перемещена или повернута. Вот как работает работа с WebGL. Так как же мы моделируем перемещение камеры в пространстве?

Вместо того, чтобы перемещать камеру назад и влево, мы применяем обратное преобразование к коробке: перемещаем коробку назад на один метр, а затем на 10 сантиметров вправо. Результат, с точки зрения каждого из двух объектов, идентичен.

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

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

В отличие от матрицы модели, которая непосредственно преобразует вершины модели, матрица вида перемещает абстрактную камеру. На самом деле, вершинный шейдер всё ещё только перемещает модели, в то время как «камера» остаётся на месте. Для правильной работы необходимо использовать обратную матрицу преобразования. Обратная матрица по существу отменяет преобразование, поэтому, если мы перемещаем вид камеры вперёд, обратная матрица заставляет объекты в сцене двигаться назад.

Следующий computeViewMatrix() метод анимирует матрицу вида, перемещая её вперёд-назад и влево-вправо.

CubeDemo.prototype.computeViewMatrix = function (now) {
  const moveInAndOut = 20 * Math.sin(now * 0.002);
  const moveLeftAndRight = 15 * Math.sin(now * 0.0017);

  // Move the camera around
  const position = MDN.translateMatrix(moveLeftAndRight, 0, 50 + moveInAndOut);

  // Multiply together, make sure and read them in opposite order
  const matrix = MDN.multiplyArrayOfMatrices([
    // Exercise: rotate the camera view
    position,
  ]);

  // Inverse the operation for camera movements, because we are actually
  // moving the geometry in the scene, not the camera itself.
  this.transforms.view = MDN.invertMatrix(matrix);
};

Теперь шейдер использует три матрицы.

gl_Position = projection * view * model * vec4(position, 1.0);

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

Результаты

Просмотр на JSFiddle

The view matrix

Связывание систем координат

На этом этапе было бы полезно сделать шаг назад и посмотреть и обозначить различные системы координат, которые мы используем. Во-первых, вершины куба определены в пространстве модели. Для перемещения модели по сцене. Эти вершины необходимо преобразовать в пространство мира, применив матрицу модели.

пространство модели → матрица модели → пространство мира

Камера ещё ничего не сделала, и точки необходимо снова переместить. В настоящее время они находятся в пространстве мира, но для представления размещения камеры их необходимо переместить в пространство вида (используя матрицу вида).

пространство мира → матрица вида → пространство вида

Наконец, необходимо добавить проекцию (в нашем случае матрицу перспективной проекции), чтобы отобразить координаты мира в координаты пространства клипа.

пространство вида → матрица проекции → пространство клипа

Упражнение

  • Перемещайте камеру по сцене.
  • Добавьте несколько матриц вращения к матрице вида, чтобы смотреть вокруг.
  • Наконец, отслеживайте позицию мыши. Используйте 2 матрицы вращения, чтобы камера смотрела вверх и вниз в зависимости от того, где находится курсор мыши на экране.

См. также

  • WebGL
  • 3D-проекция

© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_model_view_projection

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API