Эта статья продолжает серию статей о WebGL. Если вы их ещё не читали, предлагаю начать отсюда, а затем вернуться сюда.
Один из распространённых вопросов - "как добавить текст в WebGL". Сначала следует спросить себя о цели отрисовки текста. Вы работает в браузере, он и занимается отображением текста. Поэтому первый ответ на ваш вопрос - использовать HTML для отображения текста.
Для начала рассмотрим простейший пример. Вы просто хотите отобразить текст сверху вашего WebGL. Можно назвать его накладным текстом. По сути это текст, который не меняет своего положения.
Простым подходом будет использование HTML-элемента (или элементов) и CSS, чтобы текст расположился сверху.
Например, сделаем контейнер, куда поместим и canvas, и HTML, который наложится поверх canvas.
<div class="container">
<canvas id="canvas"></canvas>
<div id="overlay">
<div>Time: <span id="time"></span></div>
<div>Angle: <span id="angle"></span></div>
</div>
</div>
Далее добавим CSS, чтобы canvas и HTML пересекались друг с другом.
.container {
position: relative;
}
#overlay {
position: absolute;
left: 10px;
top: 10px;
}
Теперь при инициализации получим ссылки на эти HTML-элементы, чтобы в дальнейшем мы могли их менять.
// получаем ссылки на элементы DOM
var timeElement = document.querySelector("#time");
var angleElement = document.querySelector("#angle");
// создаём текстовые узлы, чтобы сэкономить немного браузерного времени
var timeNode = document.createTextNode("");
var angleNode = document.createTextNode("");
// добавляем текстовые узлы в нужные места
timeElement.appendChild(timeNode);
angleElement.appendChild(angleNode);
Наконец, обновляем текстовые узлы при рендеринге
function drawScene() {
...
// преобразуем значение поворота из радиан в градусы
var angle = radToDeg(rotation[1]);
// сообщаем только в диапазоне 0 - 360
angle = angle % 360;
// задаём значения узлов
angleNode.nodeValue = angle.toFixed(0); // без десятичных знаков
timeNode.nodeValue = clock.toFixed(2); // 2 десятичных знака
И вот результат
Обратите внимание, как я поместил элементы span внутрь элементов div для фрагментов, которые я хочу изменить. Полагаю, что это быстрее по сравнению с использованием только элементов div без span и написания чего-то вроде
timeNode.value = "Time " + clock.toFixed(2);
Также я использую текстовые узлы через вызов node = document.createTextNode()
,
затем присваиваю значение через node.nodeValue = someMsg
. Я мог бы использовать
someElement.innerHTML = someHTML
. Такой подход был бы более гибким, потому
что он позволяет использовать произвольный HTML, однако, он будет работать
немного медленнее, так как браузеру необходимо создавать и уничтожать узлы
при каждой смене содержимого. Что выбрать - решать вам.
Важный момент, который стоит вынести из техники наложения, это то, что WebGL работает в браузере. Не забывайте использовать возможности браузера. Большинству программистов OpenGL приходится отрисовывать каждую часть своих приложений с нуля своими силами, но браузер содержит массу готовых элементов, которые стоит использовать в своих приложениях WebGL. У такого подхода масса преимуществ. Например, можно использовать стили CSS для получения наложения.
Рассмотрим ещё один пример, где добавим несколько стилей. Закруглим углы табло, добавим свечение буквам и добавим красную границу. Всё это делается очень легко за счёт возможностей HTML.
Следующая часто используемая возможность - позиционирование текста относительно сцены. Мы также можем сделать это с помощью HTML.
Для этой задачи мы снова будем использовать контейнер с canvas и ещё одним контейнером для смещающегося элемента HTML.
<div class="container">
<canvas id="canvas" width="400" height="300"></canvas>
<div id="divcontainer"></div>
</div>
Настроим CSS
.container {
position: relative;
overflow: none;
width: 400px;
height: 300px;
}
#divcontainer {
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
z-index: 10;
overflow: hidden;
}
.floating-div {
position: absolute;
}
Стиль position: absolute;
позволяет задавать положение элементу
#divcontainer
относительно первого родителя со стилем position: relative
или position: absolute
. В этом случае им является контейнер, в котором
находятся и canvas, и #divcontainer
.
Стиль left: 0px; top: 0px
выравнивает #divcontainer
относительно
всего остального. z-index: 10
выводит контейнер на передний план
перед canvas. А overflow: hidden
обрезает дочерние элементы, которые
не помещаются в контейнер.
Наконец, .floating-div
будет использован для плавающего элемента div.
Нам осталось получить ссылку на divcontainer, создать div и добавить его.
// получаем ссылку на divcontainer
var divContainerElement = document.querySelector("#divcontainer");
// создаём div
var div = document.createElement("div");
// назначаем ему класс CSS
div.className = "floating-div";
// создаём текстовый узел для содержимого
var textNode = document.createTextNode("");
div.appendChild(textNode);
// добавляем элемент в divcontainer
divContainerElement.appendChild(div);
Теперь мы можем позиционировать div через стили.
div.style.left = Math.floor(x) + "px";
div.style.top = Math.floor(y) + "px";
textNode.nodeValue = clock.toFixed(2);
И у нас получился пример с div, отскакивающим от стен.
Предположим, что теперь нам нужно закрепить текст за конкретным участком 3D-сцены. Как это сделать? Точно так же, как и в статье о проекционной перспективе.
В том примере мы узнали, как использовать матрицы, как их умножать и как применять проекционную матрицу для конвертации в пространство отсечения. В нашем шейдере мы умножаем вершины на матрицы, и координаты преобразуются из локального пространства в пространство отсечения. Нам по силам выполнить всю математику через JavaScript. Затем мы сможем перевести координаты из пространства отсечения (от -1 до +1) в пиксели, которые в свою очередь сможем использовать для позиционирования div.
gl.drawArrays(...);
// мы только что вычислили матрицу для отрисовки буквы F в 3D
// выбираем точку в локальном пространстве буквы 'F'
// X Y Z W
var point = [100, 0, 0, 1]; // это верхний правый угол фронтальной части
// вычисляем координаты пространства отсечения,
// используя матрицу, которую мы вычисляли для F
var clipspace = m4.transformVector(matrix, point);
// делим X и Y на W аналогично видеокарте
clipspace[0] /= clipspace[3];
clipspace[1] /= clipspace[3];
// конвертация из пространства отсечения в пиксели
var pixelX = (clipspace[0] * 0.5 + 0.5) * gl.canvas.width;
var pixelY = (clipspace[1] * -0.5 + 0.5) * gl.canvas.height;
// задаём положение div
div.style.left = Math.floor(pixelX) + "px";
div.style.top = Math.floor(pixelY) + "px";
textNode.nodeValue = clock.toFixed(2);
И вуаля! Верхний правый угол нашего div расположен точно в верхнем правом углу фронтальной части буквы F.
Если мы хотим больше надписей - используем больше элементов div.
Можете посмотреть исходный код последнего примера для подробностей. Важно сказать, что я просто предполагаю, что создание, добавление или удаление элементов HTML из DOM является медленным, поэтому в примере выше элементы создаются только один раз. Неиспользуемые элементы только скрываются, но не удаляются из DOM. Вам придётся самим провести тестирование производительности, чтобы узнать, какой из подходов быстрее. Просто я для себя выбрал такой подход.
Надеюсь, вам понятно, как использовать HTML для текста. Далее рассмотрим Canvas 2D в качестве надписи.