
O artigo " Um indicador de um horizonte artificial na tela HTML5 " apresenta um código de indicador com um layout volumétrico de um objeto gerenciado baseado na invenção de A.P. Plentsov e N.A. Zakonovoy. ...
Uma das vantagens da ideia de um indicador com layout volumétrico é a sua eficácia. Desta vez, um formato incomum de visualização de horizonte artificial será adaptado para sistemas realidade aumentada .
HUD vs HDD
Recursos de design do HUD
Complementar a realidade observada com informações instrumentais é visivelmente diferente da indicação usual de valores de parâmetros. A especificidade da tarefa se reflete no design visual do HUD . Nos sistemas mais complexos e críticos ( por exemplo , no local de trabalho de um piloto de avião), como regra, uma indicação verde monocromática é usada em um projeto de "contorno".
O design minimalista do HUD é a resposta a um conjunto de requisitos de sistema conflitantes. Por exemplo, os elementos de contorno podem ter uma dimensão angular suficiente para o operador ler sem obstruir a visão do espaço externo.
Requisitos de solução
Vamos definir as principais disposições da atribuição para o desenvolvimento de uma classe de indicador de horizonte artificial:
1. O construtor da classe deve ter os seguintes argumentos:
- o tamanho da face do indicador;
- limite do valor de rotação exibido;
- o valor máximo do pitch exibido.
2. Os limites de exibição de cada ângulo são determinados por um valor, que não deve ser excedido pelo valor absoluto do valor exibido. Os valores limite não podem exceder 90 graus.
3. A escala do pitch deve ter sete marcas numéricas para o ângulo em graus. A escala da escala deve ser otimizada ao instanciar o objeto, o intervalo dos valores exibidos deve ser mínimo se as seguintes condições forem atendidas:
- as marcas superior e inferior são múltiplos de 30;
- o valor máximo do ângulo de inclinação passado ao construtor não ultrapassa a escala, inclusive quando multiplicado por -1.

4. A escala de rotação deve ter marcas em incrementos de 30 graus ao redor de toda a circunferência do mostrador, independentemente do ângulo máximo de rotação informado ao projetista. As marcas da escala de roll devem ser exibidas levando em consideração a posição de pitch do layout, ou seja, o dial deve girar no plano de simetria do local de trabalho pelo ângulo de pitch em torno do eixo que passa pelo centro do dial.

5. O modelo do veículo deve ser feito em forma de figura plana em forma de seta. A proporção entre o comprimento do layout e sua largura deve garantir o uso racional da área da tela. Por exemplo, se a escala do pitch for limitada a 90 graus, o comprimento do layout deve corresponder a cerca de metade de sua largura. Quando a escala é limitada a 30 graus, uma proporção significativa da altura da tela não é mais usada, conforme mostrado no lado direito do diagrama.

Para dimensionar adequadamente a escala com um espaçamento menor, você precisa alterar as proporções do layout.

6. A classe deve ter uma função de atualização que aceite os valores atuais dos ângulos de rotação e inclinação.

7. O indicador deve ser verde e delineado. O número de elementos indicadores deve ser o menor possível, é necessário garantir que a animação de fundo ainda esteja visível.
Resultado
Você pode avaliar o indicador resultante interativamente nas páginas do github .
O objeto neste exemplo sempre se move estritamente na direção de seu eixo longitudinal. É possível definir os valores de velocidade de movimento, ângulos de rotação e passo. O movimento é realizado apenas no plano vertical, pois o valor do ângulo de proa é constante.
Código indicador
O código de indicação do horizonte artificial é mostrado abaixo. A classe Attitude usa a biblioteca three.js .
Código de classe de atitude
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;
}
}
Analisando o código
, .
XOZ,
OZ,
z.
YOZ. z.
Attitude . . , , .
( to2D()), – add().
. . 3 .
30, 60 90 . - .
radius , skeletonLength maxPitch: , . , maxPitch.
, . , .
, .
, . .
three.js . :
1. , update(), , , . . , – .
2. , ( ), .
update() :
html. 3D .
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 .
Desvantagens do indicador
Um rápido conhecimento da demonstração interativa é suficiente para perceber as dificuldades em ler leituras em grandes ângulos absolutos:
- o início do declínio na qualidade de indicação do rolo corresponde ao ângulo de inclinação de 75-80 graus, no qual a escala do rolo torna-se visivelmente comprimida;
- o início de uma diminuição na qualidade da indicação de pequenos valores do ângulo de passo corresponde aos valores do ângulo de roll de 70-75 graus, nos quais a silhueta do modelo perde sua varredura;
- a indicação da posição invertida do objeto na solução apresentada é excluída em princípio.
Deve-se observar que não há indicação artificial do horizonte que funcione perfeitamente em qualquer posição espacial do veículo. A solução apresentada pode ser considerada adequada para uso em manobras de intensidade moderada.