Этот пост является продолжением серии постов о WebGL2. Первый начинался с основ и предыдущий был о предоставлении данных текстурам. Если вы не читали их, пожалуйста, просмотрите их сначала.
В последнем посте мы рассмотрели, как предоставлять данные из JavaScript в текстуры. В этой статье мы будем рендерить в текстуры, используя WebGL2. Обратите внимание, что эта тема была кратко рассмотрена в обработке изображений, но давайте рассмотрим ее более подробно.
Рендеринг в текстуру довольно прост. Мы создаем текстуру определенного размера
// создаем для рендеринга
const targetTextureWidth = 256;
const targetTextureHeight = 256;
const targetTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, targetTexture);
{
// определяем размер и формат уровня 0
const level = 0;
const internalFormat = gl.RGBA;
const border = 0;
const format = gl.RGBA;
const type = gl.UNSIGNED_BYTE;
const data = null;
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
targetTextureWidth, targetTextureHeight, border,
format, type, data);
// устанавливаем фильтрацию, чтобы нам не нужны были мипмапы
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
}
Обратите внимание, как data
равен null
. Нам не нужно предоставлять никаких данных. Нам просто нужно, чтобы WebGL
выделил текстуру.
Далее мы создаем framebuffer. Framebuffer - это просто коллекция вложений. Вложения
Давайте создадим framebuffer и прикрепим нашу текстуру
// Создаем и привязываем framebuffer
const fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
// прикрепляем текстуру как первое цветовое вложение
const attachmentPoint = gl.COLOR_ATTACHMENT0;
gl.framebufferTexture2D(
gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, targetTexture, level);
Так же, как текстуры и буферы, после создания framebuffer нам нужно
привязать его к точке привязки FRAMEBUFFER
. После этого все функции, связанные с
framebuffer, ссылаются на любой framebuffer, который привязан там.
С нашим привязанным framebuffer, каждый раз, когда мы вызываем gl.clear
, gl.drawArrays
или gl.drawElements
, WebGL
будет рендерить в нашу текстуру вместо холста.
Давайте возьмем наш предыдущий код рендеринга и сделаем его функцией, чтобы мы могли вызывать его дважды. Один раз для рендеринга в текстуру и снова для рендеринга в холст.
function drawCube(aspect) {
// Говорим использовать нашу программу (пару шейдеров)
gl.useProgram(program);
// Привязываем набор атрибутов/буферов, который мы хотим.
gl.bindVertexArray(vao);
// Вычисляем матрицу проекции
- var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
var projectionMatrix =
m4.perspective(fieldOfViewRadians, aspect, 1, 2000);
var cameraPosition = [0, 0, 2];
var up = [0, 1, 0];
var target = [0, 0, 0];
// Вычисляем матрицу камеры, используя look at.
var cameraMatrix = m4.lookAt(cameraPosition, target, up);
// Создаем матрицу вида из матрицы камеры.
var viewMatrix = m4.inverse(cameraMatrix);
var viewProjectionMatrix = m4.multiply(projectionMatrix, viewMatrix);
var matrix = m4.xRotate(viewProjectionMatrix, modelXRotationRadians);
matrix = m4.yRotate(matrix, modelYRotationRadians);
// Устанавливаем матрицу.
gl.uniformMatrix4fv(matrixLocation, false, matrix);
// Говорим шейдеру использовать текстуру unit 0 для u_texture
gl.uniform1i(textureLocation, 0);
// Рисуем геометрию.
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 6 * 6;
gl.drawArrays(primitiveType, offset, count);
}
Обратите внимание, что нам нужно передать aspect
для вычисления нашей матрицы проекции,
потому что наша целевая текстура имеет другой аспект, чем камера.
Вот как мы вызываем это
// Рисуем сцену.
function drawScene(time) {
...
{
// рендерим в наш targetTexture, привязывая framebuffer
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
// рендерим куб с нашей текстурой 3x2
gl.bindTexture(gl.TEXTURE_2D, texture);
// Говорим WebGL, как конвертировать из пространства отсечения в пиксели
gl.viewport(0, 0, targetTextureWidth, targetTextureHeight);
// Очищаем холст И буфер глубины.
gl.clearColor(0, 0, 1, 1); // очищаем до синего
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const aspect = targetTextureWidth / targetTextureHeight;
drawCube(aspect)
}
{
// рендерим в холст
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// рендерим куб с текстурой, в которую мы только что рендерили
gl.bindTexture(gl.TEXTURE_2D, targetTexture);
// Говорим WebGL, как конвертировать из пространства отсечения в пиксели
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// Очищаем холст И буфер глубины.
gl.clearColor(1, 1, 1, 1); // очищаем до белого
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
drawCube(aspect)
}
requestAnimationFrame(drawScene);
}
И вот результат
КРАЙНЕ ВАЖНО помнить о вызове gl.viewport
и установке его в
размер того, во что вы рендерите. В этом случае первый раз мы
рендерим в текстуру, поэтому мы устанавливаем viewport, чтобы покрыть текстуру. Второй
раз мы рендерим в холст, поэтому мы устанавливаем viewport, чтобы покрыть холст.
Аналогично, когда мы вычисляем матрицу проекции,
нам нужно использовать правильный аспект для того, во что мы рендерим. Я потерял бесчисленные
часы отладки, задаваясь вопросом, почему что-то рендерится забавно или не рендерится
совсем, только чтобы в конечном итоге обнаружить, что я забыл один или оба вызова gl.viewport
и вычисление правильного аспекта. Это так легко забыть, что теперь я стараюсь никогда не вызывать
gl.bindFramebuffer
в своем коде напрямую. Вместо этого я делаю функцию, которая делает и то, и другое,
что-то вроде
function bindFramebufferAndSetViewport(fb, width, height) {
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
gl.viewport(0, 0, width, height);
}
И тогда я использую только эту функцию для изменения того, во что я рендерю. Таким образом я не забуду.
Одна вещь, которую нужно заметить, это то, что у нас нет буфера глубины на нашем framebuffer. У нас есть только текстура. Это означает, что нет тестирования глубины и 3D не будет работать. Если мы нарисуем 3 куба, мы можем увидеть это.
Если вы посмотрите на центральный куб, вы увидите, что 3 вертикальных куба рисуются на нем, один сзади, один в середине и еще один спереди, но мы рисуем все 3 на одной глубине. Глядя на 3 горизонтальных куба, нарисованных на холсте, вы заметите, что они правильно пересекают друг друга. Это потому, что наш framebuffer не имеет буфера глубины, но наш холст имеет.
Чтобы добавить буфер глубины, мы создаем текстуру глубины и прикрепляем ее к нашему framebuffer.
// создаем текстуру глубины
const depthTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, depthTexture);
// делаем буфер глубины того же размера, что и targetTexture
{
// определяем размер и формат уровня 0
const level = 0;
const internalFormat = gl.DEPTH_COMPONENT24;
const border = 0;
const format = gl.DEPTH_COMPONENT;
const type = gl.UNSIGNED_INT;
const data = null;
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
targetTextureWidth, targetTextureHeight, border,
format, type, data);
// устанавливаем фильтрацию, чтобы нам не нужны были мипмапы
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// прикрепляем текстуру глубины к framebuffer
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, level);
}
И с этим вот результат.
Теперь, когда у нас есть буфер глубины, прикрепленный к нашему framebuffer, внутренние кубы правильно пересекаются.
Важно отметить, что WebGL гарантирует работу только определенных комбинаций вложений. Согласно спецификации единственные гарантированные комбинации вложений:
COLOR_ATTACHMENT0
= RGBA/UNSIGNED_BYTE
текстураCOLOR_ATTACHMENT0
= RGBA/UNSIGNED_BYTE
текстура + DEPTH_ATTACHMENT
= DEPTH_COMPONENT16
renderbufferCOLOR_ATTACHMENT0
= RGBA/UNSIGNED_BYTE
текстура + DEPTH_STENCIL_ATTACHMENT
= DEPTH_STENCIL
renderbufferДля любых других комбинаций вы должны проверить, поддерживает ли система/GPU/драйвер/браузер пользователя эту комбинацию. Чтобы проверить, вы создаете свой framebuffer, создаете и прикрепляете вложения, затем вызываете
var status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
Если статус FRAMEBUFFER_COMPLETE
, то эта комбинация вложений работает для этого пользователя.
В противном случае она не работает, и вам придется сделать что-то еще, например, сказать пользователю, что ему не повезло,
или переключиться на какой-то другой метод.
Если вы еще не проверили упрощение WebGL с меньшим количеством кода, больше веселья.