Как стать автором
Обновить

3D-индикатор крена и тангажа для HUD на Three.js

JavaScript *Работа с 3D-графикой *Разработка игр *HTML *WebGL *
Браузерные игры с трехмерной графикой создаются достаточно давно. Существуют и симуляторы различных транспортных средств, где игроку необходимо контролировать пространственное положение управляемого объекта.



В статье «Индикатор искусственного горизонта на HTML5 canvas» представлен код индикатора с объемным макетом управляемого объекта на основе изобретения Пленцова А. П. и Законовой Н. А. В реальной технике такая индикация распространения не получила, но в компьютерных играх она вполне может быть использована.

К числу достоинств идеи индикатора с объемным макетом следует отнести эффектность. На этот раз необычный формат визуализации искусственного горизонта будет адаптирован для систем дополненной реальности.

HUD vs HDD
Дизайн индикатора, копирующий вид лицевой части электромеханического прибора, подходит для вывода на простом экране или head down display (HDD). HDD обладают недостатком обычных приборных панелей: взгляд оператора фиксируется или на инструментальной информации, или на внешнем мире, с заметными переходами между этими двумя режимами восприятия.

Сегодня даже в автомобилях широко используется индикация на лобовом стекле (head up display или HUD – дословно «экран поднятой головы»), позволяющая минимизировать затраты времени и усилий оператора на переключение внимания.

Формату индикации на лобовом стекле посвящено большое количество исследований. Примеры современных публикаций:


Особенности оформления HUD


Дополнение наблюдаемой реальности инструментальной информацией заметно отличается от обычной индикации значений параметров. Специфика задачи отражается на визуальном оформлении HUD. В наиболее сложных и ответственных системах (например, на рабочем месте пилота авиалайнера), как правило, используют монохромную зеленую индикацию в «контурном» исполнении.

Минимализм дизайна HUD – ответ на комплекс противоречивых требований к системе. Например, контурные элементы способны иметь достаточный для считывания оператором угловой размер, не перекрывая при этом обзор внешнего пространства.

Требования к решению


Определим ключевые положения задания на разработку класса индикатора искусственного горизонта:

1. Конструктор класса должен иметь следующие аргументы:

  • размер лицевой части индикатора;
  • предельное отображаемое значение крена;
  • предельное отображаемое значение тангажа.

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

3. Шкала тангажа должна иметь семь числовых отметок значений угла в градусах. Масштаб шкалы должен быть оптимизирован при инстанцировании объекта, интервал отображаемых значений должен быть минимальным при соблюдении следующих условий:

  • верхняя и нижняя отметки кратны 30;
  • максимальное значение угла тангажа, переданное конструктору, не выходит за пределы шкалы, в том числе при умножении на -1.




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



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



Для правильного масштабирования шкалы с меньшим интервалом необходимо изменить пропорции макета.



6. В классе должна присутствовать функция обновления, принимающая текущие значения углов крена и тангажа.



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

Результат


Оценить получившийся индикатор в интерактивном режиме можно на github pages.

Объект в этом примере движется всегда строго в направлении своей продольной оси. Имеется возможность задавать значения скорости движения, углов крена и тангажа. Движение осуществляется только в вертикальной плоскости, поскольку значение угла курса постоянно.

Код индикатора


Код индикации искусственного горизонта представлен ниже. В классе Attitude используется библиотека three.js.

Код класса Attitude
class Attitude {
    constructor(camera, scene, radius, maxPitch, maxRoll) {
        //тангаж:
        //устанавливаем кратный 30 с запасом в большую сторону предел значений угла:
        if (maxPitch > 90) maxPitch = 90;
        this.maxPitch = maxPitch;
        maxPitch /= 30;
        maxPitch = Math.ceil(maxPitch) * 30;

        //крен:
        if (maxRoll > 90) maxRoll = 90;
        this.maxRoll = maxRoll;

        //определяем длину силуэта:
        let skeletonLength = radius / Math.sin(maxPitch * Math.PI / 180);
        //строим контурный силуэт:
        let geometry = new THREE.Geometry();
        geometry.vertices.push(new THREE.Vector3(0, 0, -skeletonLength / 4));
        geometry.vertices.push(new THREE.Vector3(-radius, 0, 0));
        geometry.vertices.push(new THREE.Vector3(0, 0, -skeletonLength));
        geometry.vertices.push(new THREE.Vector3(radius, 0, 0));
        geometry.vertices.push(new THREE.Vector3(0, 0, -skeletonLength / 4)); // замыкаем контур
        //контурный материал:
        let material = new THREE.LineBasicMaterial({ color: 0x00ff00, linewidth: 1 });
        //создаем линию контура:
        this.skeleton = new THREE.Line(geometry, material);
        scene.add(this.skeleton);

        //создаем шкалу тангажа:
        let pitchScaleStep = maxPitch / 3;

        let textLabelsPos = [];//позиции текстовых меток шкалы
        for (let i = 0; i < 7; i++) {
            let lineGeometry = new THREE.Geometry();

            //левый и правый края линии метки:
            let leftPoint = new THREE.Vector3(-radius / 10,
                skeletonLength * Math.sin((maxPitch - pitchScaleStep * i) * Math.PI / 180),
                -skeletonLength * Math.cos((maxPitch - pitchScaleStep * i) * Math.PI / 180));
            let rightPoint = new THREE.Vector3();
            rightPoint.copy(leftPoint);
            rightPoint.x += (radius / 5);
            //линия метки:
            lineGeometry.vertices.push(leftPoint);
            lineGeometry.vertices.push(rightPoint);
            let line = new THREE.Line(lineGeometry, material);
            scene.add(line);
            //позиция текстовой метки
            let textPos = new THREE.Vector3();
            textPos.copy(leftPoint);
            textLabelsPos.push(textPos);
        }

        //создаем шкалу крена:
        let rollScaleStep = 30;
        this.rollLines = [];
        for (let i = 0; i < 12; i++) {
            if (i != 3 && i != 9) {//не ставим верхнюю и нижнюю метки
                let lineGeometry = new THREE.Geometry();
                //края линии метки:
                lineGeometry.vertices.push(new THREE.Vector3(-Math.cos(
                    i * rollScaleStep * Math.PI / 180) * radius * 1.1,
                    Math.sin(i * rollScaleStep * Math.PI / 180) * radius * 1.1,
                    0));
                lineGeometry.vertices.push(new THREE.Vector3(-Math.cos(
                    i * rollScaleStep * Math.PI / 180) * radius * 0.9,
                    Math.sin(i * rollScaleStep * Math.PI / 180) * radius * 0.9,
                    0));

                this.rollLines.push(new THREE.Line(lineGeometry, material));
                scene.add(this.rollLines[this.rollLines.length - 1]);
            }
        }

        //текстовые метки:
        for (let i = 0; i < 7; i++) {
            let labelText = document.createElement('div');
            labelText.style.position = 'absolute';
            labelText.style.width = 100;
            labelText.style.height = 100;
            labelText.style.color = "Lime";
            labelText.style.fontSize = window.innerHeight / 35 + "px";
            labelText.innerHTML = Math.abs(maxPitch - pitchScaleStep * i);

            let position3D = textLabelsPos[i];
            let position2D = to2D(position3D);

            labelText.style.top = (position2D.y) * 100 / window.innerHeight - 2 + '%';
            labelText.style.left = (position2D.x) * 100 / window.innerWidth - 4 + '%';
            document.body.appendChild(labelText);
        }

        function to2D(pos) {
            let vector = pos.project(camera);
            vector.x = window.innerWidth * (vector.x + 1) / 2;
            vector.y = -window.innerHeight * (vector.y - 1) / 2;
            return vector;
        }

    }

    update(roll, pitch) {
        //проверка выхода за ограничение:
        if (pitch > this.maxPitch) pitch = this.maxPitch;
        if (pitch < -this.maxPitch) pitch = -this.maxPitch;

        if (roll > this.maxRoll) roll = this.maxRoll;
        if (roll < -this.maxRoll) roll = -this.maxRoll;

        //установка силуэта в положение, соответствующее текущим значениям крена и тангажа
        this.skeleton.rotation.z = -roll * Math.PI / 180;
        this.skeleton.rotation.x = pitch * Math.PI / 180;

        //перемещаем только отметки крена:
        let marksNum = this.rollLines.length;
        for (let i = 0; i < marksNum; i++)
            this.rollLines[i].rotation.x = pitch * Math.PI / 180;
    }
}


Разбор кода
Индикатор размещен таким образом, что центр циферблата шкалы крена совпадает с началом мировой системы координат. При отсутствии тангажа и крена макет лежит в плоскости XOZ, его продольная ось совпадает с осью OZ, нос макета направлен в сторону отрицательных значений z.

Плоскостью симметрии рабочего места оператора будем считать плоскость YOZ. Взгляд оператора направлен в сторону отрицательных значений z.

Основную часть кода класса Attitude составляет его конструктор. Здесь определены геометрические свойства и дизайн всех элементов индикатора. Помимо параметров, указанных в требованиях, конструктору передаются камера и сцена.

constructor(camera, scene, radius, maxPitch, maxRoll){ 

Первая понадобится для проекции трехмерного положения текстовых отметок на экран (функция to2D()), а вторая – для добавления к ней всех видимых компонентов индикатора через метод add().

Установка пределов шкал выполняется довольно просто. Для шкалы тангажа дополнительно устанавливаются границы по условиям п. 3 требований.

 if (maxPitch > 90) maxPitch = 90;
        this.maxPitch = maxPitch;
        maxPitch /= 30;
        maxPitch = Math.ceil(maxPitch) * 30;

Шкала тангажа может иметь предел 30, 60 или 90 градусов. Для рационального использования пространства экрана пропорции макета-стрелки должны выбираться в соответствии с этими значениями.

let skeletonLength = radius / Math.sin(maxPitch * Math.PI / 180);

Если параметр radius однозначно определяет ширину макета, то длина skeletonLength зависит от maxPitch: чем выше максимальное значение тангажа, тем короче макет. Таким образом, пропорции самого индикатора не зависят от maxPitch.

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

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

 let geometry = new THREE.Geometry();
        geometry.vertices.push(new THREE.Vector3(0, 0, -skeletonLength / 4));
        geometry.vertices.push(new THREE.Vector3(-radius, 0, 0));
        geometry.vertices.push(new THREE.Vector3(0, 0, -skeletonLength));
        geometry.vertices.push(new THREE.Vector3(radius, 0, 0));
        geometry.vertices.push(new THREE.Vector3(0, 0, -skeletonLength / 4));
        let material = new THREE.LineBasicMaterial({ color: 0x00ff00, linewidth: 1 });
        this.skeleton = new THREE.Line(geometry, material);
        scene.add(this.skeleton);

Обход силуэта осуществляется по часовой стрелке, если смотреть на него сверху. Повторение в коде первой точки замыкает контур фигуры.

Аналогичные средства three.js используются для создания линий отметок шкал. Шкалы тангажа и крена имеют следующие различия:

1. Шкала тангажа неподвижна, положение ее элементов не меняется при вызове метода update(), а все метки шкалы крена, кроме двух нулевых, вращаются по тангажу. Различие обусловлено природой самих углов. Вращение по тангажу осуществляется вокруг поперечной оси нормальной земной системы координат, а вращение по крену – вокруг продольной оси связанной системы координат.

2. Отметки шкалы тангажа расположены горизонтально, перпендикулярно плоскости шкалы (плоскости симметрии рабочего места оператора), а отметки шкалы крена находятся в ее плоскости и имеют радиальное направление.

Метод update() после проверки значений тангажа и крена на соответствие установленным ограничениям осуществляет поворот всех подвижных компонентов:

  • макет поворачивается на углы крена и тангажа;
  • шкала крена поворачивается на угол тангажа.

Текстовые подписи числовых значений шкалы тангажа выполняются посредством создания тегов html. Использование 3D шрифтов в этом случае лишено практического смысла.

Недостатки индикатора


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

  • начало снижения качества индикации крена соответствует значениям угла тангажа 75-80 градусов, при которых шкала крена становится заметно сжатой;
  • начало снижения качества индикации малых значений угла тангажа соответствует значениям угла крена 70-75 градусов, при которых силуэт макета теряет стреловидность;
  • индикация перевернутого положения объекта в представленном решении исключена в принципе.

Стоит отметить, что индикации искусственного горизонта, идеально работающей в любых пространственных положениях транспортного средства, не существует. Представленное решение можно считать пригодным для использования на маневрах умеренной интенсивности.
Теги:
Хабы:
Всего голосов 4: ↑4 и ↓0 +4
Просмотры 1.4K
Комментарии Комментировать