Это список анти-паттернов для WebGL. Анти-паттерны - это вещи, которых следует избегать
Добавление viewportWidth
и viewportHeight
в WebGLRenderingContext
Некоторые коды добавляют свойства для ширины и высоты viewport
и прикрепляют их к WebGLRenderingContext
примерно так:
gl = canvas.getContext("webgl2");
gl.viewportWidth = canvas.width; // ПЛОХО!!!
gl.viewportHeight = canvas.height; // ПЛОХО!!!
Затем они могут делать что-то вроде этого:
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
Почему это плохо:
Это объективно плохо, потому что теперь у вас есть 2 свойства, которые нужно обновлять
каждый раз, когда вы изменяете размер canvas. Например, если вы измените размер
canvas, когда пользователь изменяет размер окна, gl.viewportWidth
и gl.viewportHeight
будут неправильными, если вы не установите их снова.
Это субъективно плохо, потому что любой новый WebGL программист взглянет на ваш код
и, вероятно, подумает, что gl.viewportWidth
и gl.viewportHeight
являются частью спецификации WebGL,
что будет путать их месяцами.
Что делать вместо этого:
Зачем создавать себе больше работы? WebGL контекст имеет доступ к своему canvas, и у него есть размер.
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
Контекст также имеет свою ширину и высоту прямо на нем.
// Когда вам нужно установить viewport в соответствии с размером drawingBuffer canvas,
// это всегда будет правильно
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
Еще лучше, это будет обрабатывать крайние случаи, тогда как использование gl.canvas.width
и gl.canvas.height
не будет. Что касается почему, см. здесь.
Использование canvas.width
и canvas.height
для соотношения сторон
Часто код использует canvas.width
и canvas.height
для соотношения сторон, как здесь:
var aspect = canvas.width / canvas.height;
perspective(fieldOfView, aspect, zNear, zFar);
Почему это плохо:
Ширина и высота canvas не имеют ничего общего с размером, в котором canvas отображается. CSS контролирует размер, в котором отображается canvas.
Что делать вместо этого:
Используйте canvas.clientWidth
и canvas.clientHeight
. Эти значения говорят вам, какого
размера ваш canvas фактически отображается на экране. Используя эти значения,
вы всегда получите правильное соотношение сторон независимо от настроек CSS.
var aspect = canvas.clientWidth / canvas.clientHeight;
perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);
Вот примеры canvas, чьи drawingbuffer’ы имеют одинаковый размер (width="400" height="300"
),
но используя CSS мы сказали браузеру отображать canvas другого размера.
Обратите внимание, что образцы оба отображают ‘F’ в правильном соотношении сторон.
Если бы мы использовали canvas.width
и canvas.height
, это было бы не так.
Использование window.innerWidth
и window.innerHeight
для вычисления чего-либо
Многие WebGL программы используют window.innerWidth
и window.innerHeight
во многих местах.
Например:
canvas.width = window.innerWidth; // ПЛОХО!!
canvas.height = window.innerHeight; // ПЛОХО!!
Почему это плохо:
Это не переносимо. Да, это может работать для WebGL страниц, где вы хотите сделать canvas заполняющим экран. Проблема возникает, когда вы этого не делаете. Может быть, вы решите сделать статью как эти уроки, где ваш canvas - это просто небольшая диаграмма на большей странице. Или, может быть, вам нужен редактор свойств сбоку или счет для игры. Конечно, вы можете исправить свой код для обработки этих случаев, но почему бы просто не написать его так, чтобы он работал в этих случаях с самого начала? Тогда вам не придется изменять какой-либо код, когда вы копируете его в новый проект или используете старый проект по-новому.
Что делать вместо этого:
Вместо того чтобы бороться с веб-платформой, используйте веб-платформу так, как она была предназначена для использования.
Используйте CSS и clientWidth
и clientHeight
.
var width = gl.canvas.clientWidth;
var height = gl.canvas.clientHeight;
gl.canvas.width = width;
gl.canvas.height = height;
Вот 9 случаев. Все они используют точно такой же код. Обратите внимание, что ни один из них
не ссылается на window.innerWidth
или window.innerHeight
.
Страница только с canvas, использующая CSS для полноэкранного режима
Страница с canvas, встроенным в абзац
Страница с canvas, встроенным в абзац, использующим box-sizing: border-box;
box-sizing: border-box;
заставляет границы и отступы занимать место от элемента, на котором они определены, а не снаружи него. Другими словами, в
обычном режиме box-sizing элемент 400x300 пикселей с 15-пиксельной границей имеет 400x300 пикселей пространства содержимого, окруженного 15-пиксельной границей, что делает его общий размер
430x330 пикселей. В режиме box-sizing: border-box граница идет изнутри, так что тот же элемент останется 400x300 пикселей, содержимое окажется
370x270. Это еще одна причина, почему использование clientWidth
и clientHeight
так важно. Если вы установите границу, скажем, 1em
, у вас не будет
способа узнать, какого размера окажется ваш canvas. Это было бы разным с разными шрифтами на разных машинах или разных браузерах.
Страница с контейнером, встроенным в абзац, в который код вставит canvas
Страница без элементов с настройкой CSS для полноэкранного режима, в которую код вставит canvas
Опять же, суть в том, что если вы принимаете веб и пишете свой код, используя методы выше, вам не придется изменять какой-либо код, когда вы столкнетесь с разными случаями использования.
Использование события 'resize'
для изменения размера вашего canvas.
Некоторые приложения проверяют событие 'resize'
окна, как здесь, для изменения размера их canvas.
window.addEventListener('resize', resizeTheCanvas);
или так:
window.onresize = resizeTheCanvas;
Почему это плохо:
Это не плохо само по себе, скорее, для большинства WebGL программ это подходит для меньшего количества случаев использования.
Конкретно 'resize'
работает только когда окно изменяет размер. Это не работает,
если canvas изменяет размер по какой-то другой причине. Например, скажем, вы делаете
3D редактор. У вас canvas слева и настройки справа. Вы сделали так, что есть перетаскиваемая полоса,
разделяющая 2 части, и вы можете перетащить эту полосу, чтобы сделать область настроек больше или меньше.
В этом случае вы не получите никаких событий 'resize'
. Аналогично, если у вас есть страница, где другое содержимое
добавляется или удаляется, и canvas изменяет размер, когда браузер перераспределяет страницу, вы не получите событие resize.
Что делать вместо этого:
Как и многие решения анти-паттернов выше, есть способ написать ваш код так, чтобы он просто работал для большинства случаев. Для WebGL приложений, которые постоянно рисуют каждый кадр, решение - проверять, нужно ли изменять размер каждый раз, когда вы рисуете, как здесь:
function resizeCanvasToDisplaySize() {
var width = gl.canvas.clientWidth;
var height = gl.canvas.clientHeight;
if (gl.canvas.width != width ||
gl.canvas.height != height) {
gl.canvas.width = width;
gl.canvas.height = height;
}
}
function render() {
resizeCanvasToDisplaySize();
drawStuff();
requestAnimationFrame(render);
}
render();
Теперь в любом из этих случаев ваш canvas будет масштабироваться до правильного размера. Нет необходимости изменять какой-либо код для разных случаев. Например, используя тот же код из #3 выше, вот редактор с изменяемой областью редактирования.
Не было бы событий resize для этого случая, ни для любого другого, где canvas изменяет размер на основе размера других динамических элементов на странице.
Для WebGL приложений, которые не перерисовывают каждый кадр, код выше все еще правильный, вам просто нужно
запустить перерисовку в каждом случае, где canvas может потенциально изменить размер. Один простой способ - использовать ResizeObserver
const resizeObserver = new ResizeObserver(render); resizeObserver.observe(gl.canvas, {box: 'content-box'});
Добавление свойств к WebGLObject
’ам
WebGLObject
’ы - это различные типы ресурсов в WebGL, такие как WebGLBuffer
или WebGLTexture
. Некоторые приложения добавляют свойства к этим объектам. Например, код вроде этого:
var buffer = gl.createBuffer();
buffer.itemSize = 3; // ПЛОХО!!
buffer.numComponents = 75; // ПЛОХО!!
var program = gl.createProgram();
...
program.u_matrixLoc = gl.getUniformLocation(program, "u_matrix"); // ПЛОХО!!
Почему это плохо:
Причина, по которой это плохо, в том, что WebGL может “потерять контекст”. Это может произойти по любой
причине, но самая распространенная причина - если браузер решит, что используется слишком много ресурсов GPU,
он может намеренно потерять контекст на некоторых WebGLRenderingContext
’ах, чтобы освободить место.
WebGL программы, которые хотят всегда работать, должны обрабатывать это. Google Maps обрабатывает это, например.
Проблема с кодом выше в том, что когда контекст потерян, WebGL функции создания, такие как
gl.createBuffer()
выше, будут возвращать null
. Это эффективно делает код таким:
var buffer = null;
buffer.itemSize = 3; // ОШИБКА!
buffer.numComponents = 75; // ОШИБКА!
Это, вероятно, убьет ваше приложение с ошибкой вроде:
TypeError: Cannot set property 'itemSize' of null
Хотя многим приложениям все равно, если они умрут, когда контекст потерян, кажется плохой идеей писать код, который придется исправлять позже, если разработчики когда-либо решат обновить их приложение для обработки событий потери контекста.
Что делать вместо этого:
Если вы хотите держать WebGLObject
’ы и некоторую информацию о них вместе, один способ был бы
использовать JavaScript объекты. Например:
var bufferInfo = {
id: gl.createBuffer(),
itemSize: 3,
numComponents: 75,
};
var programInfo = {
id: program,
u_matrixLoc: gl.getUniformLocation(program, "u_matrix"),
};
Лично я бы предложил использовать несколько простых помощников, которые делают написание WebGL намного проще.
Это несколько из того, что я считаю WebGL анти-паттернами в коде, который я видел в сети. Надеюсь, я объяснил, почему их следует избегать, и дал решения, которые просты и полезны.