Содержание

WebGL2Fundamentals.org

Fix, Fork, Contribute

Советы по WebGL2

Эта статья — сборник мелких проблем, с которыми вы можете столкнуться при работе с WebGL, но которые слишком малы для отдельной статьи.


Как сделать скриншот канваса

В браузере есть по сути две функции для создания скриншота: Старая — canvas.toDataURL и новая, более удобная — canvas.toBlob

Кажется, что сделать скриншот просто — достаточно добавить такой код:

<canvas id="c"></canvas>
+<button id="screenshot" type="button">Сохранить...</button>
const elem = document.querySelector('#screenshot');
elem.addEventListener('click', () => {
  canvas.toBlob((blob) => {
    saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
  });
});

const saveBlob = (function() {
  const a = document.createElement('a');
  document.body.appendChild(a);
  a.style.display = 'none';
  return function saveData(blob, fileName) {
     const url = window.URL.createObjectURL(blob);
     a.href = url;
     a.download = fileName;
     a.click();
  };
}());

Вот пример из статьи про анимацию с этим кодом и немного CSS для кнопки:

Когда я попробовал — получил вот такой скриншот:

Да, это просто пустое изображение.

Возможно, у вас сработает (зависит от браузера/ОС), но обычно не работает.

Проблема в том, что для производительности и совместимости браузер по умолчанию очищает буфер рисования WebGL-канваса после отрисовки.

Есть три решения:

  1. вызвать функцию рендера прямо перед захватом

    Код, который мы использовали, был функцией drawScene. Лучше сделать так, чтобы эта функция не меняла состояние, и тогда можно вызывать её для захвата.

    elem.addEventListener('click', () => {
    +  drawScene();
      canvas.toBlob((blob) => {
        saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
      });
    });
    
  2. вызвать код захвата внутри рендер-цикла

    В этом случае мы просто ставим флаг, что хотим сделать захват, а в рендер-цикле реально делаем захват:

    let needCapture = false;
    elem.addEventListener('click', () => {
       needCapture = true;
    });
    

    а в рендер-цикле (например, в drawScene), после отрисовки:

    function drawScene(time) {
      ...
    
    +  if (needCapture) {
    +    needCapture = false;
    +    canvas.toBlob((blob) => {
    +      saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
    +    });
    +  }
    
      ...
    }
    
  3. Установить preserveDrawingBuffer: true при создании WebGL-контекста

    const gl = someCanvas.getContext('webgl2', {preserveDrawingBuffer: true});
    

    Это заставит WebGL не очищать канвас после композитинга с остальной страницей, но может помешать некоторым оптимизациям.

Я бы выбрал вариант №1. Для этого лучше разделить код, который обновляет состояние, и код, который рисует.

  var then = 0;

-  requestAnimationFrame(drawScene);
+  requestAnimationFrame(renderLoop);

+  function renderLoop(now) {
+    // Переводим в секунды
+    now *= 0.001;
+    // Разница со временем предыдущего кадра
+    var deltaTime = now - then;
+    // Запоминаем время
+    then = now;

+    // Каждый кадр увеличиваем вращение
+    rotation[1] += rotationSpeed * deltaTime;

+    drawScene();

+    // Следующий кадр
+    requestAnimationFrame(renderLoop);
+  }

  // Рисуем сцену
+  function drawScene() {
- function drawScene(now) {
-    // Переводим в секунды
-    now *= 0.001;
-    // Разница со временем предыдущего кадра
-    var deltaTime = now - then;
-    // Запоминаем время
-    then = now;
-
-    // Каждый кадр увеличиваем вращение
-    rotation[1] += rotationSpeed * deltaTime;

    webglUtils.resizeCanvasToDisplaySize(gl.canvas);

    ...

-    // Следующий кадр
-    requestAnimationFrame(drawScene);
  }

Теперь можно просто вызвать drawScene перед захватом:

elem.addEventListener('click', () => {
+  drawScene();
  canvas.toBlob((blob) => {
    saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
  });
});

Теперь всё должно работать.

Если посмотреть на полученное изображение, фон будет прозрачным. Подробнее — в этой статье.


Как не очищать канвас

Допустим, вы хотите дать пользователю рисовать анимированным объектом. Нужно передать preserveDrawingBuffer: true при создании WebGL-контекста. Это не даст браузеру очищать канвас.

Возьмём последний пример из статьи про анимацию:

var canvas = document.querySelector("#canvas");
-var gl = canvas.getContext("webgl2");
+var gl = canvas.getContext("webgl2", {preserveDrawingBuffer: true});

и изменим вызов gl.clear, чтобы очищался только depth-буфер:

-// Очищаем канвас.
-gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+// Очищаем только depth-буфер.
+gl.clear(gl.DEPTH_BUFFER_BIT);

Обратите внимание: если делать полноценную программу для рисования, это не решение, так как браузер всё равно очистит канвас при изменении его размера. Мы меняем размер в зависимости от размера отображения, а он меняется при изменении окна, загрузке файла (например, в другой вкладке), появлении статус-бара, повороте телефона и т.д.

Если делать настоящее приложение для рисования — рендерьте в текстуру.


Получение ввода с клавиатуры

Если вы делаете полноэкранное WebGL-приложение, то всё просто. Но часто хочется, чтобы канвас был частью страницы, и чтобы при клике по нему он принимал ввод с клавиатуры. По умолчанию канвас не получает ввод с клавиатуры. Чтобы это исправить, задайте ему tabindex 0 или больше. Например:

<canvas tabindex="0"></canvas>

Появляется новая проблема: любой элемент с tabindex получает обводку при фокусе. Чтобы убрать её, добавьте CSS:

canvas:focus {
  outline:none;
}

Для примера — три канваса:

<canvas id="c1"></canvas>
<canvas id="c2" tabindex="0"></canvas>
<canvas id="c3" tabindex="1"></canvas>

и CSS только для последнего:

#c3:focus {
    outline: none;
}

Навесим одинаковые обработчики на все:

document.querySelectorAll('canvas').forEach((canvas) => {
  const ctx = canvas.getContext('2d');

  function draw(str) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(str, canvas.width / 2, canvas.height / 2);
  }
  draw(canvas.id);

  canvas.addEventListener('focus', () => {
    draw('has focus press a key');
  });

  canvas.addEventListener('blur', () => {
    draw('lost focus');
  });

  canvas.addEventListener('keydown', (e) => {
    draw(`keyCode: ${e.keyCode}`);
  });
});

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


Создание WebGL-анимации как фона

Часто спрашивают, как сделать WebGL-анимацию фоном веб-страницы.

Есть два очевидных способа:

  • Установить CSS position канваса как fixed:
#canvas {
 position: fixed;
 left: 0;
 top: 0;
 z-index: -1;
 ...
}

и установить z-index в -1.

Небольшой недостаток этого решения: ваш JavaScript должен интегрироваться со страницей, и если у вас сложная страница, нужно убедиться, что JavaScript в WebGL-коде не конфликтует с JavaScript, который делает другие вещи на странице.

  • Использовать iframe

Это решение используется на главной странице этого сайта.

В вашей веб-странице просто вставьте iframe, например:

<iframe id="background" src="background.html"></iframe>
<div>
  Ваш контент здесь.
</div>

Затем стилизуйте iframe, чтобы он заполнял окно и был на фоне — в основном тот же код, что мы использовали выше для канваса, но также нужно установить border в none, так как iframe по умолчанию имеют границу:

#background {
    position: fixed;
    width: 100vw;
    height: 100vh;
    left: 0;
    top: 0;
    z-index: -1;
    border: none;
    pointer-events: none;
}

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