Indicador de cabeceo y balanceo 3D para HUD en Three.js

Los juegos de navegador con gráficos en 3D existen desde hace mucho tiempo. También hay simuladores de varios vehículos, donde el jugador necesita controlar la posición espacial del objeto controlado.







El artículo " Un indicador de un horizonte artificial en el lienzo HTML5 " presenta un código indicador con un diseño volumétrico de un objeto gestionado basado en la invención de A.P. Plentsov y N.A. Zakonovoy. En la tecnología real, tal indicación no ha recibido distribución, pero en los juegos de computadora bien puede usarse ...



Una de las ventajas de la idea de un indicador con diseño volumétrico es su efectividad. Esta vez, se adaptará un formato de visualización de horizonte artificial inusual para los sistemas realidad aumentada .



HUD vs HDD
, , head down display (HDD). HDD : , , .



( head up display HUD – « »), .



. :





Características de diseño de HUD



Complementar la realidad observada con información instrumental es notablemente diferente de la indicación habitual de los valores de los parámetros. La especificidad de la tarea se refleja en el diseño visual del HUD . En los sistemas más complejos y críticos ( por ejemplo , en el lugar de trabajo de un piloto de avión), por regla general, se utiliza una indicación verde monocromática en un diseño de "contorno".



El diseño minimalista del HUD es la respuesta a una serie de requisitos del sistema en conflicto. Por ejemplo, los elementos del contorno pueden tener una dimensión angular suficiente para que el operador pueda leer sin obstruir la vista del espacio exterior.



Requisitos de la solución



Definamos las disposiciones clave de la asignación para el desarrollo de una clase de indicador de horizonte artificial:



1. El constructor de la clase debe tener los siguientes argumentos:



  • el tamaño de la cara del indicador;
  • límite del valor de rollo mostrado;
  • el valor máximo de tono mostrado.


2. Los límites de visualización de cada ángulo están determinados por un valor, que no debe ser superado por el valor absoluto del valor visualizado. Los valores límite no pueden exceder los 90 grados.



3. La escala de tono debe tener siete marcas numéricas para el ángulo en grados. La escala de la escala debe optimizarse al crear una instancia del objeto, el intervalo de los valores mostrados debe ser mínimo si se cumplen las siguientes condiciones:



  • las marcas superior e inferior son múltiplos de 30;
  • el valor máximo del ángulo de paso que se pasa al constructor no va más allá de la escala, incluso cuando se multiplica por -1.








4. La escala de balanceo debe tener marcas en incrementos de 30 grados alrededor de toda la circunferencia del cuadrante, independientemente del ángulo máximo de balanceo informado al diseñador. Las marcas de la escala de balanceo deben mostrarse teniendo en cuenta la posición de paso del diseño, es decir, el dial debe girar en el plano de simetría del lugar de trabajo por el ángulo de paso alrededor del eje que pasa por el centro del dial.







5. El modelo del vehículo debe tener la forma de una figura plana en forma de flecha. La relación entre la longitud del diseño y su ancho debe garantizar el uso racional del área de la pantalla. Por ejemplo, si la escala de tono se limita a 90 grados, la longitud del diseño debe corresponder aproximadamente a la mitad de su ancho. Cuando la escala está limitada a 30 grados, ya no se usa una proporción significativa de la altura de la pantalla, como se muestra en el lado derecho del diagrama.







Para escalar correctamente la escala con un espaciado más pequeño, debe cambiar las proporciones del diseño.







6. La clase debe tener una función de actualización que acepte los valores actuales de los ángulos de alabeo y cabeceo.







7. El indicador debe ser verde y contorneado. El número de elementos indicadores debe ser lo más pequeño posible, es necesario asegurarse de que la animación de fondo aún sea visible.



Resultado



Puede evaluar el indicador resultante de forma interactiva en las páginas de github .



El objeto de este ejemplo siempre se mueve estrictamente en la dirección de su eje longitudinal. Es posible configurar los valores de velocidad de movimiento, ángulos de balanceo y cabeceo. El movimiento se realiza únicamente en el plano vertical, ya que el valor del ángulo de rumbo es constante.



Código indicador



El código de indicación de horizonte artificial se muestra a continuación. La clase Attitude usa la biblioteca three.js .



Código de clase de actitud
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;
    }
}

      
      







Analizando el código
, . 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 .



Desventajas del indicador



Un conocimiento rápido de la demostración interactiva es suficiente para notar las dificultades en la lectura de lecturas en ángulos absolutos grandes:



  • el comienzo de la disminución en la calidad de la indicación de balanceo corresponde al ángulo de cabeceo de 75-80 grados, en el que la escala de balanceo se comprime notablemente;
  • el comienzo de una disminución en la calidad de la indicación de pequeños valores del ángulo de inclinación corresponde a los valores del ángulo de balanceo de 70-75 grados, en los que la silueta del modelo pierde su barrido;
  • La indicación de la posición invertida del objeto en la solución presentada se excluye en principio.


Cabe señalar que no existe ninguna indicación de horizonte artificial que funcione perfectamente en cualquier posición espacial del vehículo. La solución presentada puede considerarse adecuada para su uso en maniobras de intensidad moderada.



All Articles