Содержание

WebGL2Fundamentals.org

Fix, Fork, Contribute

WebGL2 Анти-паттерны

Это список анти-паттернов для WebGL. Анти-паттерны - это вещи, которых следует избегать

  1. Добавление 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 не будет. Что касается почему, см. здесь.

  2. Использование 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, это было бы не так.

  3. Использование 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, установленным на использование 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. Это было бы разным с разными шрифтами на разных машинах или разных браузерах.

    Страница только с контейнером, использующим CSS для полноэкранного режима, в который код вставит canvas

    Страница с контейнером, установленным на использование 70% ширины, чтобы было место для элементов управления редактора, в который код вставит canvas

    Страница с контейнером, встроенным в абзац, в который код вставит canvas

    Страница с контейнером, встроенным в абзац, использующим box-sizing: border-box;, в который код вставит canvas

    Страница без элементов с настройкой CSS для полноэкранного режима, в которую код вставит canvas

    Опять же, суть в том, что если вы принимаете веб и пишете свой код, используя методы выше, вам не придется изменять какой-либо код, когда вы столкнетесь с разными случаями использования.

  4. Использование события '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'});
    
  5. Добавление свойств к 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 анти-паттернами в коде, который я видел в сети. Надеюсь, я объяснил, почему их следует избегать, и дал решения, которые просты и полезны.

Что такое drawingBufferWidth и drawingBufferHeight?

GPU имеют ограничение на то, насколько большим прямоугольником пикселей (текстура, renderbuffer) они могут поддерживать. Часто этот размер - это следующая степень 2 больше, чем какое-либо распространенное разрешение монитора было во время создания GPU. Например, если GPU был разработан для поддержки экранов 1280x1024, он может иметь ограничение размера 2048. Если он был разработан для экранов 2560x1600, он может иметь ограничение 4096.

Это кажется разумным, но что происходит, если у вас несколько мониторов? Скажем, у меня есть GPU с ограничением 2048, но у меня два монитора 1920x1080. Пользователь открывает окно браузера с WebGL страницей, затем растягивает это окно на оба монитора. Ваш код пытается установить canvas.width в canvas.clientWidth, который в этом случае равен 3840. Что должно произойти?

Сразу на ум приходят только 3 варианта

  1. Выбросить исключение.

    Это кажется плохим. Большинство веб-приложений не будут проверять это, и приложение упадет. Если у приложения были пользовательские данные, пользователь только что потерял свои данные

  2. Ограничить размер canvas до лимита GPU

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

  3. Позволить canvas быть того размера, который запросил пользователь, но сделать его drawingbuffer лимитом

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

У большинства людей нет нескольких мониторов, поэтому эта проблема редко возникает. Или, по крайней мере, раньше. Chrome и Safari, по крайней мере, по состоянию на январь 2015 года, имели жестко закодированное ограничение на размер canvas в 4096. Apple's 5k iMac превышает этот лимит. Много WebGL приложений имели странные дисплеи из-за этого. Аналогично многие люди начали использовать WebGL с несколькими мониторами для установочной работы и сталкивались с этим лимитом.

Итак, если вы хотите обрабатывать эти случаи, используйте gl.drawingBufferWidth и gl.drawingBufferHeight, как показано в #1 выше. Для большинства приложений, если вы следуете лучшим практикам выше, вещи просто будут работать. Имейте в виду, хотя, если вы делаете вычисления, которые должны знать фактический размер drawingbuffer, вам нужно учитывать это. Примеры сразу на ум, [picking](webgl-picking.html), другими словами преобразование из координат мыши в координаты пикселей canvas. Другим был бы любой вид пост-обработки эффектов, которые хотят знать фактический размер drawingbuffer.

Есть предложения или замечания? Создайте issue на GitHub.
comments powered by Disqus