оглавление

WebGLFundamentals.org

Изменение размера Canvas в WebGL

Вот, что вам следует знать об изменениях размера canvas.

Каждый canvas имеет 2 размера. Первый - размер буфера отрисовки. Он отвечает за то, сколько пикселей помещается в canvas. Второй - размер отображаемого на HTML-странице элемента canvas. Этот размер задаётся через CSS.

Размер буфера отрисовки можно задать двумя способами. Первый - через HTML:

<canvas id="c" width="400" height="300"></canvas>

Второй способ - через JavaScript:

<canvas id="c" ></canvas>

JavaScript

var canvas = document.getElementById("c");
canvas.width = 400;
canvas.height = 300;

Что касается размера отображаемого элемента, то при отсутствии стилей CSS, которые влияют на элемент, размер элемента будет равен размеру буфера отрисовки. Поэтому в 2 примерах выше размер буфера отрисовки будет равен 400x300, и размер отображаемого элемента будет также равен 400x300.

А вот пример, когда размер буфера отображения - 10x15, а размер HTML-элемента - 400x300 пикселей:

<canvas id="c" width="10" height="15" style="width: 400px; height: 300px;"></canvas>

или можно задать так:

<style>
#c {
  width: 400px;
  height: 300px;
}
</style>
<canvas id="c" width="10" height="15"></canvas>

Если мы отобразим вращающуюся линию шириной один пиксель, мы получим что-то вроде:

Почему она такая размытая? Потому что браузер принимает canvas 10x15, а затем растягивает его до размера 400x300 пикселей, применяя для этого фильтрацию.

Так что же делать, если, к примеру, нам нужно, чтобы canvas заполнил всё окно? Для начала нам нужны CSS-стили, чтобы браузер растянул canvas на всё окно. Например:

<html>
  <head>
    <style>
      /* убираем границу */
      body {
        border: 0;
        background-color: white;
      }
      /* растягиваем canvas на всю область просмотра */
      canvas {
        width: 100vw;
        height: 100vh;
        display: block;
      }
    <style>
  </head>
  <body>
    <canvas id="c"></canvas>
  </body>
</html>

Теперь нам нужно, чтобы размер буфера отрисовки соответствовал размеру HTML-элемента, как бы браузер его ни растянул. Мы можем использовать свойствами clientWidth и clientHeight, которые есть у любого HTML-элемента и которые позволяют JavaScript узнавать размер отображаемого элемента.

function resize(canvas) {
  // получаем размер HTML-элемента canvas
  var displayWidth  = canvas.clientWidth;
  var displayHeight = canvas.clientHeight;

  // проверяем, отличается ли размер canvas
  if (canvas.width  != displayWidth ||
      canvas.height != displayHeight) {

    // подгоняем размер буфера отрисовки под размер HTML-элемента
    canvas.width  = displayWidth;
    canvas.height = displayHeight;
  }
}

Большинство приложений WebGL используют анимацию, поэтому будем вызывать эту функцию непосредственно перед отрисовкой, чтобы при отрисовке размер canvas всегда соответствовал размеру HTML-элемента.

function drawScene() {
   resize(gl.canvas);

   ...

И вот результат:

Но что не так? Почему линия не занимает всю область?

Причина в том, что при изменении размера canvas нам также необходимо вызвать gl.viewport для установки размера области просмотра. gl.viewport сообщает WebGL, как и в какую область внутри canvas преобразовывать координаты пространства отсечения (от -1 до +1) в пиксели. При первом создании контекста WebGL область просмотра будет соответствовать размеру canvas, но после этого вам необходимо следить за её размерами. Если вы меняете размер canvas, вам также необходимо сообщить WebGL новые параметры области просмотра.

Изменим код, чтобы учитывать такую ситуацию. Более того, раз уж ссылка на canvas доступна из контекста WebGL, будем передавать его параметром функции resize.

function drawScene() {
   resize(gl.canvas);

+   gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
   ...

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

Откройте пример в отдельном окне и меняйте размер окна. Линия всегда заполняет всё окно.

Слышу, вы спрашиваете, почему WebGL не устанавливает область просмотра для нас автоматически при изменении размера canvas? Просто потому, что он не знает, как и для чего вы используете область просмотра. Например, вы можете выполнять отрисовку во фреймбуфер или заниматься чем-то другим, что требует различного размера области просмотра. WebGL никак не может знать ваши намерения, поэтому он не подстраивает область просмотра.

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

Что насчёт дисплеев Retina или HD-DPI?

При указании размера через CSS или Canvas используются пиксели, называемые также CSS-пиксели, которые могут отличаться от действительных пикселей. Большинство современных смартфонов оснащены дисплеями высокой чёткости (HD-DPI) или, как их называет Apple, "дисплеями Retina". Для текста и большей части стилей CSS браузер способен автоматически отображать графику HD-DPI, но в случае с WebGL вы сами отвечаете за графику, и вам необходимо самим позаботиться об отображении графики с более высоким разрешением, если вы хотите получить качество HD-DPI.

Для этого мы можем посмотреть на значение window.devicePixelRatio. Это значение укажет, как много действительных пикселей помещаются в одном пикселе CSS. Мы можем внести изменения в функцию изменения размера для ориентации на действительный пиксель.

function resize(gl) {
  var realToCSSPixels = window.devicePixelRatio;

  // Берём заданный браузером размер canvas в CSS-пикселях и вычисляем нужный
  // нам размер, чтобы буфер отрисовки совпадал с ним в действительных пикселях
  var displayWidth  = Math.floor(gl.canvas.clientWidth  * realToCSSPixels);
  var displayHeight = Math.floor(gl.canvas.clientHeight * realToCSSPixels);

  //  проверяем, отличается ли размер canvas
  if (gl.canvas.width  !== displayWidth ||
      gl.canvas.height !== displayHeight) {

    // подгоняем размер буфера отрисовки под размер HTML-элемента
    gl.canvas.width  = displayWidth;
    gl.canvas.height = displayHeight;
  }
}

Если вы откроете эту страницу на дисплее HD-DPI - например, на вашем смартфоне - вы заметите, что линия на примере ниже тоньше, чем в примерах выше, в которых не учитывались дисплеи HD-DPI.

Настраивать ли программу под использование дисплеев HD-DPI, зависит только от вас. На iPhone4 и iPhone5 window.devicePixelRatio равен 2, что означает, что вы будете отображать в 4 раза больше пикселей. Думаю, что на iPhone6Plus это значение равно 3, то есть вы будете отображать в 9 раз больше пикселей. Это может замедлить вашу програму. Вообще, для игр типично отображать меньше пикселей, чтобы видеокарта сама растянула их. Всё зависит от ваших целей. Например, если вы готовите график для печати, то, возможно, вам понадобится поддержка HD-DPI. Для игр, возможно, не понадобится такой поддержки, или вы сделаете настройку, чтобы можно было включить поддержку HD-DPI или выключить, если устройство недостаточно мощное для отрисовки такого количества пикселей.

Вопросы? Спросите на stackoverflow.
Нашли ошибку? Создайте задачу на github.
comments powered by Disqus