Ниже приведён список антипаттернов в WebGL. Антипаттерны - это вещи, которых стоит избегать.
Добавление свойств viewportWidth
и viewportHeight
объекту WebGLRenderingContext
Порой можно встретить, что значения ширины и высоты области просмотра
записываются в свойства объекта WebGLRenderingContext
, например так:
gl = canvas.getContext("webgl"); gl.viewportWidth = canvas.width; // ПЛОХО!!! gl.viewportHeight = canvas.height; // ПЛОХО!!!
Затем свойства могут использоваться следующим образом:
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
Почему это плохо:
Это объективно плохо, потому что теперь у вас появляется 2 свойства, которые
нужно не забывать обновлять каждый раз при изменении размера canvas. Например,
при изменении размера окна пользователем свойства gl.viewportWidth
и
gl.viewportHeight
будут некорректными, их нужно обновить согласно новым
размерам canvas.
Это субъективно плохо, потому что новички в WebGL посмотрят на ваш код и
подумают, что gl.viewportWidth
и gl.viewportHeight
- часть спецификации
WebGL, что введёт их в заблуждение, возможно, на месяцы.
Что делать вместо этого:
Зачем создавать себе больше работы? Контекст WebGL уже содержит ширину и высоту. Просто используйте их.
// если вам необходимо, чтобы область просмотра соответствовала размеру // буфера отрисовки 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 отображается на странице. За размер canvas на странице отвечает CSS.
Что делать вместо этого:
Используйте canvas.clientWidth
и canvas.clientHeight
. Эти размеры вернут
реальный размер отображаемого на экране canvas. Используя эти свойства, у вас
всегда будет правильное соотношение сторон экрана, независимо от CSS.
var aspect = canvas.clientWidth / canvas.clientHeight; perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);
Ниже идут несколько примеров canvas с одинаковым размером (width="400" height="300"
),
но через CSS мы задаём разный размер отображения в браузере. Заметьте, что
примеры сохраняют корректное соотношение сторон экрана.
А если мы используем canvas.width
и canvas.height
, изображение искажается.
Использование для вычислений window.innerWidth
и window.innerHeight
Многие программы WebGL используют window.innerWidth
и window.innerHeight
во многих местах. Например:
canvas.width = window.innerWidth; // ПЛОХО!! canvas.height = window.hinnerHeight; // ПЛОХО!!
Почему это плохо:
Это не переносится. Да, это будет работать на HTML-страницах, где вы хотите растянуть 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 размером 70% ширины страницы, чтобы осталось место для контрольной панели
Страница с 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
Повторюсь, что смысл в том, чтобы при написании кода, используя описанные техники, вам не пришлось менять код, сталкиваясь с различными ситуациями.
Использование события 'resize'
для изменения размера canavs
Некоторые приложения отслеживают событие 'resize'
, чтобы подстроить размер canavs.
window.addEventListener('resize', resizeTheCanvas);
или так
window.onresize = resizeTheCanvas;
Почему это плохо:
Само по себе это не плохо. Просто для большинства программ на WebGL такое
решение не охватывает некоторых ситуаций. Событие 'resize'
срабатывает, только
когда размер окна меняется. Оно не сработает, если по каким-то причинам изменился
размер canvas. Например, вы делаете 3D-редактор. Ваш canvas находится слева,
настройки расположены справа. И у вас есть возможность расширять или сужать
панель настроек, потянув за границу между настройками и canvas. В этом случае
не будет никакого события 'resize'
. Аналогично, при добавлении или удалении
контента на страницу размер canvas может меняться, и вы тоже не получите события
'resize'
.
Что делать вместо этого:
Как и во многих решениях выше, в данном случае есть способ написать код так, чтобы он работал в большинстве ситуаций. Для приложений WebGL, которые постоянно отображают каждый кадр, можно проверять размер перед отрисовкой, и в зависимости от результата обновлять размер.
function resize() { 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() { resize(); drawStuff(); requestAnimationFrame(render); } render();
Теперь во всех описанных выше ситуациях canvas будет всегда иметь правильный размер. Нет необходимости менять код для отдельных случаев. Например, используя код из примера #3, сделаем редактор с возможностью менять размер панели настроек.
В этом примере не будет события изменений размера, как и в других случаях, когда размер canvas меняется динамически в зависимости от других элементов страницы.
Для приложений, где не отрисовывается каждый кадр, код по-прежнему будет работать, вам нужно лишь вызывать перерисовку при изменении размера canvas. Одним из решений будет использование цикла requestAnimationFrame следующим образом:
function resize() { 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; return true; } return false; } var needToRender = true; // необходимо отрисовать хотя бы раз function checkRender() { if (resize() || needToRender) { needToRender = false; drawStuff(); } requestAnimationFrame(checkRender); } checkRender();
Здесь отрисовка будет происходить только в том случае, когда размер canvas изменился
или значение needToRender
равно true. Таким образом мы решим проблему изменения
размера в приложениях, которые не отрисовывают каждый кадр. Просто установите
needToRender
в значение true, когда на сцене что-то изменилось, и вы хотите
отрисовать сцену, чтобы отразить эти изменения.
Добавление свойств к объектам 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 может "потерять контекст"". Это может случиться по
многим причинам, но чаще всего возникает ситуация, когда браузер решает, что используется
слишком много ресурсов видеокарты, и вытесняет контекст некоторых WebGLRenderingContext
,
чтобы освободить память. Программам WebGL, работающим без остановки, необходимо отлавливать
эту ситуацию. Например, Google Maps обрабатывает такую ситуацию.
Проблема упомянутого кода в том, что при потере контекста теряются и функции создания
объектов, например gl.createBuffer()
вернёт null
. Поэтому следующий код
var buffer = null; buffer.itemSize = 3; // ОШИБКА! buffer.numComponents = 75; // ОШИБКА!
скорей всего прекратит выполнение вашего приложение с ошибкой
TypeError: Cannot set property 'itemSize' of null
Многим приложениям не важно, если они завершатся по ошибке при потере контекста, но всё же это является плохой практикой, потому что разработчику придётся исправлять код, если ему понадобится обрабатывать ситуацию с потерей контекста.
Что делать вместо этого:
Если вам нужно сохранить WebGLObjects
и другую информации в одном месте, одним
из способов будет использование объектов JavaScript. Например:
var bufferInfo = { id: gl.createBuffer(), itemSize: 3, numComponents: 75, }; var programInfo = { id: program, u_matrixLoc: gl.getUniformLocation(program, "u_matrix"), };
Лично я бы предложил использовать несколько простых помощников, которые облегчат написание кода WebGL.
Описанные выше ситуации, которые я отношу к антипаттернам WebGL, я встретил на просторах интернета. Надеюсь, я показал, почему их нужно избегать, а приведённые решение будут вам полезны.