Pull to refresh

HTML и CSS безумие [перевод]

Reading time 6 min
Views 106K

или Создаем 3D миры при помощи HTML, CSS и JS


image
В прошлом году, я сделал демо, которое показывает как можно использовать CSS 3D transforms для создания 3D пространства. Демо было технической демонстрацией того, чего можно достичь при помощи CSS на то время, но я хотел увидеть насколько далеко я могу зайти, поэтому последние несколько месяцев я работал над новой версией с еще более сложными моделями, реалистичным освещением, тенями и определением столкновений. Этот пост документирует то, как я это делал и какие техники применял.

Демо Демо2

Создаем 3D объекты


В современных 3D движках объекты хранятся в виде набора точек (или векторов), каждая имеет X, Y и Z значение для декларации своей позиции в 3D пространстве. Квадрат, к примеру, будет определен 4-мя векторами, по одному для каждого угла. Каждым из векоторов можно манипулировать в индивидуальном порядке, перемещая его по X, Y и Z осям, позволяя тем самым квадрату вытягиваться в различные фигуры. Визуализатор 3D движка будет использовать эти векторы и множество умной математики чтобы нарисовать 3D объект на Вашем 2D экране.
С CSS трансформациями все наоборот. Мы не можем задавать произвольные фигуры набором точек, наши руки связаны HTML элементами которые постоянно квадратные и имеют двухмерные свойства, такие как top, left, width и height для обозначения их позиций и размеров. Но во многом, это делает работу с 3D намного проще, так как в таком случае нет сложной математики — просто нужно применить CSS трансформацию для поворота элемента вокруг осей и готово!
Создавать объекты из квадратов может сначала показаться ограниченным методом, но Вы можете создать удивительное их количество, особенно когда начнете играть с PNG альфа каналами. На рисунке снизу Вы можете наблюдать, как верхняя часть бочки и колесо кажутся круглыми, не смотря на то что созданы из квадратов.

image

пример 3D объектов, созданных полностью из квадратных <div> элементов


Все объекты созданы при помощи JavaScript, используя набор методов для создания примитивной геометрии. Самый простой объект, который может быть создан, — это плоскость, которая по существу обычный <div> элемент. Плоскости могут быть добавлены в наборы, обертка <div> над ними позволяет всему объекту вращаться и перемещаться как одной сущности. Труба это набор плоскостей, повернутых вокруг осей и бочка это труба с плоскостью на верху и плоскостью внизу.

Данный пример показывает сказанное на практике, взгляните на вкладку JS.

Свет


Свет был самым сложным вызовом в этом проекте. Не буду врать, математика почти сломала меня, но это стоило того, потому что свет привнес невероятное ощущение глубины и атмосферы в плоское и безжизненное пространство.

image
скриншот комнаты без освещения


Как я уже говорил, объект в обычном 3D движке задается сериями векторов. Чтобы высчитать свет, эти векторы используются для подсчета «нормали» которая может быть использована для определения количества света, которое отражается от центральной точки поверхности объекта.
Это формулирует проблему, когда создаешь 3D объекты при помощи HTML элементов, потому что этих векторов не существуют. Итак первое препятствие — написание набора методов для вычисления четырех векторов (по одному на каждый угол) для элемента который был трансформирован при помощи CSS, по которым может быть рассчитан свет. Как только я это определил, сразу начал экспериментировать с различными способами освещать объекты.

В моем первом эксперименте я использовал несколько фоновых изображений для имитации света падающего на поверхность, путем объединения linear-gradient с картинкой. Эффект использует градиент, который начинается и заканчивается с тем же rgba значением, производя сплошной цветовой блок. Измение значения альфа канала позволяет лежащему ниже изображению просвечиваясь через цветовой блок, создавать иллюзию затенения.

image
пример использования градиента для затенения текстуры


Чтобы достичь максимально темного эффекта на изображении сверху я применил слудующие стили:
element {
    background: linear-gradient(rgba(0,0,0,.8), rgba(0,0,0,.8)), url("texture.png");
} 

Практически, эти стили не задекларированы заранее, они высчитываются динамически и применяются прямо к аттрибуту style элемента, используя JavaScript.
Эта технику называют плоским затенением (flat shading). Это эффективный метод затенения, но его результат — вся поверхность имеет одинаковую детализацию. Например, если я создаю 3D стену которая отдаляется на определенное расстояние, она будет затенена одинаково на протяжении всей своей длины. Мне хотелось сделать что-нибудь более реалистичное.

Второй штурм освещения


Чтобы имитировать реальное освещение, поверхности необходимо затемняться при отдалении от источника света, и если мы имеем несколько таких источников, падающих на поверхность, она должна быть затенена соответственно.
Для плоской затененной поверхности, мне нужно было только рассчитать свет, падающий на центральную точку, но теперь мне нужно замерять свет в различных точках поверхности, чтобы определить, насколько освещенной или затененной должна быть каждая из точек. Математика требовала создания этой световой информации так же, как и в случае плоского затенения. Я пытался создавать radial-gradient из световой информации, чтобы использовать его на месте linear-gradient моей предидущей попытки. Результаты были более реалистичны, но множественные источники света были до сих пор проблемой, так как наслаивание нескольких градиентов один на другой постепенно затемняет низлежащие текстуры. Если бы CSS поддерживало совмещение изображений и режимы смешивания(blending они на подходе), имелась бы возможность заставить радиальные градиенты работать.
Решением было использование <canvas> элемента, для программной генерации новой текстуры, которая может быть использована как световая карта. При помощи рассчитанной световой информации я смог нарисовать наборы черных пикселей, у каждого изменяя альфа канал, исходя из количества света, которое должно падать на поверхность в данной точке.
В конце я использовал canvas.toDataURL() метод для закодирования изображения, которое использовал вместо linear-gradient моего первого эксперимента. Повторяя этот процесс, для каждой поверхности, я воспроизвел эффект реалистичного освещения для всего пространства эксперимента.
Высчитывание и рисование таких тесктур — напряженная работа. Потолок и пол подвала, оба имеют размер в 1000х2000 пикселей, создаая текстуру для покрытия всей этой площади не очень практично, поэтому я замеряю свет только каждые 12 пикселей, что производит световую карту в 12 раз меньшую, чем поверхность, которую она покроет.
Установка background-size: 100% заставляет браузер масштабировать текстуру, используя билинейную (или похожую) фильтрацию, поэтому световая карта покрывает всю нужную нам поверхность. Эффект масштабирования создает результат который практически полностью идентичен к световой карте, сгенерированной для каждого пикселя.

Пример правила стиля для фона, которое применяется для задания световой карты и поверхности, выглядит примерно так:

element {
    background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAyCAYAAAAqRkmtAAACiUlEQVRoQ9VZa0vEQAy8+/8/yPcbFVFEEREREcS/4eNSmpKmmU12t/XYD6W3beGmk8kk2a5Xq9XR5vjeHD+B47d/xjrTtdSxud3dl+d+OTmt+yvDmX4c9n8eAbtVoAeCyRRYBknMb4XRfRVyC6wEqYFK0Cj0HG4OfSr8HG56ZhR6AhoJO2u4JPxInxK4BDYCyYu9YOglk/xbsymvy8RhpjlrNEArqWRCrWlBQEsYRWC98EfAmlm/W8FoBKzWp2aT11KbZugJ6NKMzpJMFlANPJX1Wpdyjdj0NKozv9PoTjD0pRYVDb2b9VGgVtZHNKrLpuelEzb5DRhoRKel1alGox1wGfpmgFIYPbCIUa+MRiuTpdMJox7QrSdTM/bUlOFzZ7SERqM+6pbQZpqSZtq8ZhrnZkaRZoa7ZsblZjYgjgM1vmYClYOd1+LBnpRM9iQItLTFQ/0o6vLhXH9aCTTaOWnAehp1K9NZP4rUbufo2UnP9ajV82b6Tg70FudiZtKtHmrtoiOIpU9PpzD0Fwoo2n6sbZo9gJJZcwq9DGi0JpFyhjuoU7pxtSCjtXP9aDfv2gFaOoJofaKst5JJ+ukwM91kMirtyMp0a5MsOtyZicSobxMaLd3K0dZksZlt+HcZoUdsImZTY0g20PtA6OeypohFQR99MCqTDnnqA0NKp7My+ggYjerz34A+LQRUsolCrnWaNPznSqCoeyo1e/bV0T4+LV4KgCJwJAPNZI41WfV+MPzXAFDLluY2e83kqDoR2jcD6ByJhJoRrUtea31OgL4roCU9aORDWMRDIav0Fh890JR3lnz/1GVU1nsGlJX1n4UaRQkV6ZpQ+Uwm05ej0TkSycp8i2Hoo3+utUtvDhk9pwAAAABJRU5ErkJggg==") 0 0 / 100% 100%, url("texture.png") 0 0 / auto auto;
}


Что создает освещенную поверхность:

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


Наложение теней


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

image
скриншот результирующей комнаты с освещением и тенями


Определение столкновений


Для определения столкновений используется карта высоты — изображение ниже использует цвет для отображения высоты объектов на нем. Белый цвет отображает наиболее глубокую, а черный наиболее высокую из возможных позиций, которую игрок может достичь. По мере того как играющий двигается по уровню, я конвертирую его позицию в 2D координаты и использую их для проверки цвета на карте высоты. Если цвет светлее чем последняя позиция игрока, он падает, если цвет немного темнее, игрок может подняться или подпрыгнуть на объект. Если цвет значительно темнее, игрок останавливается, я пользуюсь этим для задания стен и препятствий. Это изображение нарисованно от руки, но оно смотрится так же, как и созданное динамически.

image
изображение карты высоты и ее отношение к уровням


Что дальше?


Ну, полноценная игра будет логичным следующим шагом — будет интересно посмотреть, насколько масштабируемы эти техники. Пока что я начал работать над прототипом CSS3 renderer  для восхитительной Three.js  которая использует те же техники для рендеринга геометрии и света, созданных настоящим 3D движком.

первоисточник
Tags:
Hubs:
+189
Comments 71
Comments Comments 71

Articles