Oтзывчивый Дизайн

Это вторая статья в серии статей о three.js. Первая была об основах. Если вы её еще не читали, советую вам сделать это.

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

Для three.js нужно рассмотреть еще больше ситуаций. Например, 3D-редактор с элементами управления слева, справа, сверху или снизу - это то, что мы можем захотеть обработать. Динамическая диаграмма в середине документа является еще одним примером.

В последнем примере мы использовали простой холст без указания CSS и размера

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

Этот холст по умолчанию имеет размер 300x150 CSS пикселей.

В вебе рекомендуемый способ установить размер чего-либо - использовать CSS.

Прим. переводчика: Далее идет слишком подробное описание банальностей.

Давайте растянем холст на всю страницу:

<style>
html, body {
   margin: 0;
   height: 100%;
}
#c {
   width: 100%;
   height: 100%;
   display: block;
}
</style>

HTML body по умолчанию имеет margin в 5 пикселей, поэтому установка в 0 удаляет margin. Установка высоты html и body на 100% заставляет их заполнить окно. В противном случае они будут такими же большими, как контент, который их наполняет.

Далее мы говорим, что id=c элемент будет занимать 100% его контейнера, который в данном случае является телом документа.

Наконец, мы установили его display режим на block. Режим отображения canvas по умолчанию inline. Inline элементы могут в конечном итоге добавить пробел к тому, что отображается. Устанавливая canvas'у display: block эта проблема исчезает.

Вот результат

Вы можете видеть, что холст сейчас заполняет страницу, но есть 2 проблемы.

  1. Кубы растягиваются. Они не кубики, они больше похожи на прямоугольные параллелепипеды. Слишком высокие или слишком широкие. Откройте пример в его отдельном окне и измените его размер. Вы увидите, как кубы растягиваются то вширь, то ввысь.

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

Давайте сначала исправим проблему растяжения. Для этого нам нужно установить соотношение сторон (aspect) камеры в соответствии с размером холста. Мы можем сделать это, задав холсту свойства clientWidth и clientHeight.

Мы обновим наш цикл отрисовки следующим образом

function render(time) {
  time *= 0.001;

+  const canvas = renderer.domElement;
+  camera.aspect = canvas.clientWidth / canvas.clientHeight;
+  camera.updateProjectionMatrix();

  ...

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

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

Теперь давайте исправим пиксильность.

Canvas элемент имеет 2 размера. Один размер - это размер холста, отображаемый на странице. Это то, что мы устанавливаем с помощью CSS. Другой размер - это количество пикселей на холсте. Это ничем не отличается от изображения. Например, у нас может быть изображение размером 128x64 пикселей, а с помощью css мы можем отобразить как 400x200 пикселей.

<img src="some128x64image.jpg" style="width:400px; height:200px">

Внутренний размер холста, его разрешение часто называют размером рисованного буфера (drawingbuffer size). В three.js мы можем установить размер буфера рисования холста, вызывая renderer.setSize. Какой размер мы должны выбрать? Самый очевидный ответ - "тот же размер, что отображается на холсте". Опять же, чтобы сделать это, мы можем посмотреть на clientWidth и clientHeight свойства.

Давайте напишем функцию, которая проверяет, совпадает ли размер рисованного буфера с размером, на котором он отображается, и, если это так, зададим холсту этот размер.

function resizeRendererToDisplaySize(renderer) {
  const canvas = renderer.domElement;
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;
  const needResize = canvas.width !== width || canvas.height !== height;
  if (needResize) {
    renderer.setSize(width, height, false);
  }
  return needResize;
}

Обратите внимание, что мы проверяем, нужно ли изменять размер холста. Изменение размера холста - интересная часть спецификации холста, и лучше не устанавливать тот же размер, если он уже соответствует желаемому.

Как только мы узнаем, нужно ли нам изменить размер или нет, мы вызываем renderer.setSize и передаем новую ширину и высоту. Важно передать false в конце. render.setSize по умолчанию устанавливает размер CSS холста, но это не то, что нам нужно. Мы хотим, чтобы браузер продолжал работать так же, как и для всех других элементов, то есть использовать CSS для определения размера отображения элемента. Мы не хотим, чтобы холсты, используемые three.js, отличались от других элементов.

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

function render(time) {
  time *= 0.001;

+  if (resizeRendererToDisplaySize(renderer)) {
+    const canvas = renderer.domElement;
+    camera.aspect = canvas.clientWidth / canvas.clientHeight;
+    camera.updateProjectionMatrix();
+  }

  ...

Поскольку apsect будет меняться только в случае изменения размера холста, мы устанавливаем соотношение сторон камеры, только если resizeRendererToDisplaySize вернёт true.

Теперь он должен отображаться с разрешением, соответствующим размеру изображения на холсте.

Чтобы сделать так, чтобы CSS позволял обрабатывать изменение размера, давайте возьмем наш код и поместим его в отдельный .js файл. Вот еще несколько примеров, где мы позволяем CSS выбирать размер, без написания кода.

Давайте поместим наши кубики в середине абзаца текста.

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

Важная часть, на которую следует обратить внимание - отсутствие изменений прежнего кода. Только HTML и CSS изменились.

Обработка дисплеев HD-DPI

HD-DPI - дисплеи с высокой плотностью точек на дюйм. Сейчас они у большинства компьютеров Mac, многих компьютеров с Windows, а также почти всех смартфонов.

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

Существуют различные способы обработки HD-DPI с помощью three.js.

Первый - просто не делать ничего особенного. Это, пожалуй, самый распространенный. Рендеринг 3D-графики занимает много вычислительной мощности графического процессора. Мобильные графические процессоры имеют меньшую мощность, чем настольные компьютеры, по крайней мере на 2018 год, и все же мобильные телефоны часто имеют дисплеи с очень высоким разрешением. Нынешние топовые телефоны имеют соотношение HD-DPI 3x, означающее, что для каждого пикселя с дисплея без HD-DPI эти телефоны имеют 9 пикселей. Это означает, что они должны сделать 9-кратный рендеринг.

Вычисление 9x пикселей - большая работа, поэтому, если мы просто оставим код таким, какой он есть, мы вычислим 1x пикселей, а браузер просто нарисует его в 3x размере (3x на 3x = 9x пикселей).

Для любого тяжелого приложения three.js это, вероятно, то, что вам нужно, иначе вы, вероятно, получите медленную частоту кадров (FPS).

Тем не менее, если вы действительно хотите рендерить с разрешением устройства, есть три способа сделать это в three.js.

Один из них заключается в том, чтобы сообщить Three.js множитель разрешения, используя renderer.setPixelRatio. Вы спрашиваете браузер, каков множитель пикселей CSS для пикселей устройства, и передаете его в three.js.

 renderer.setPixelRatio(window.devicePixelRatio);

После этого любые вызовы renderer.setSize будут магически использовать размер, который вы запрашиваете, умноженный на любое количество пикселей, которое вы передали.

Другой способ - сделать это самостоятельно, когда вы измените размер холста.

    function resizeRendererToDisplaySize(renderer) {
      const canvas = renderer.domElement;
      const pixelRatio = window.devicePixelRatio;
      const width = canvas.clientWidth * pixelRatio | 0;
      const height = canvas.clientHeight * pixelRatio | 0;
      const needResize = canvas.width !== width || canvas.height !== height;
      if (needResize) {
        renderer.setSize(width, height, false);
      }
      return needResize;
    }

Я предпочитаю этот второй способ. Зачем? Потому что это означает, что я получаю то, что я прошу. При использовании three.js существует много случаев, когда нам нужно знать фактический размер canvas's drawingBuffer. Например, при создании фильтра пост-обработки, или если мы создаем шейдер, который получает доступ gl_FragCoord и прочее... Делая это самостоятельно, мы всегда знаем, что используемый размер - это размер, который мы запрашивали. Не существует особого случая, когда магия происходит за кулисами.

Вот пример использования кода выше.

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

Эта статья охватывает очень основную, но фундаментальную тему. Далее давайте быстро пройдемся по основным примитивам, которые предоставляет three.js.