Olá pessoal! Sou o líder da equipe para o desenvolvimento de aplicativos de desktop na empresa da Rogia Europe. Desenvolvemos soluções de software para o setor de petróleo e gás.
Aconteceu que o nosso principal produto StarSteer não possui um painel de correlação - uma ferramenta clássica para guias de poços. A tarefa foi adiada por muito tempo por causa de outras mais prioritárias, mas no outono passado finalmente conseguimos iniciá-la.
Ignorando as perguntas da base de código herdada - mencionarei no artigo - havia uma pergunta fundamental - que tecnologia devo usar? Definitivamente, precisamos do OpenGL - que já é usado no MapView e na visualização 3d com base no OpenSceneGraph -, mas é óbvio que não está vazio e com elementos de interface gráfica. O OSG caiu = (. Uma tecnologia que atende a dois requisitos - gráfico de cena e GUI no OpenGL - eu conhecia apenas um - Qt QML / Quick. Sobre o que temos e o que temos que compartilhar - dentro.
Entrada
Começamos a desenvolver o produto no outono de 2013. Que conjunto de bibliotecas usar para mim, como fã do Qt, não era sequer uma pergunta. Nesse momento, a pergunta pode surgir: use a quarta ou a quinta (ainda bastante recente) versão do Qt. Escolhemos o quinto e, no auge do nosso vôo, só posso dizer: graças a Deus!
Todo o visual é desenvolvido no QtGui / Widgets. Todas as cenas em que os gráficos são exibidos (gama, porosidade, resistência etc.) são feitas no QGraphicsScene / View. Meu conselho é - não use este pacote para coisas sérias! Argumentos: barras de rolagem e não desconectadas da parte externa (está fora sem as próprias edições do Qt) lógica para centralizar a cena ( qgraphicsview.cpp +458 e superior no mesmo método para horizontal; qgraphicsview.cpp +3816 - que controle sobre a matriz). Se isso não incomodá-lo, use - muitas peças convenientes da caixa.
O que mais não usar? NSIS.
Tudo estava perfeito, o produto estava em desenvolvimento, as tarefas estavam sendo feitas, o número de clientes estava crescendo. Refatoração ... Em geral, depois de algum tempo, o QGraphicsScene começou a interferir conosco - não tínhamos pressa de otimizar o desenho de gráficos de 40.000 pontos, quando as linhas grossas eram ativadas - todo esse material na CPU era muito lento.
Ao longo do caminho, nos cansamos de desenvolver o GUY em widgets. Com as mãos completamente no código ou um pouco no designer (do Creator, .ui + .cpp). Eu queria coisas modernas, como uma descrição declarativa da interface gráfica.
Fizemos uma lista de tecnologias nas quais faremos:
- a maneira antiquada no QGraphicsView / Scene;
- para cada faixa, use um QOpenGLWidget vazio separado;
- implementar a janela inteira no QOpenGLWidget, mas a GUI por nós mesmos (ou encontrar algo);
- dois pontos anteriores + OSG, respectivamente;
- Qt QML / Quick,
apenas seis pontos; discutido. Meu carisma superou e decidi tentar ver como o protótipo na QML se comporta.
Protótipo
Abri um exemplo scenegraph \ graph. Parecia e fechado =). Joguei por vários dias, olhei para outros exemplos, mas nada se aproximou do meu querido objetivo.
E o que era necessário então? Aqui está a aparência do painel de correlação:
Uma estrutura bastante simples é uma lista de trilhas de poço, dentro de trilhas de curvas, escalas; Bem, as pequenas coisas. Muito texto, no futuro, alguns controles - botões, listas suspensas, campos de entrada e muito mais.
Eu olhei para o sgengine, aprendi a criar dois gráficos de cena e desenhá-los na viewport designada. Mais tarde, percebi que, com essa opção, o QML / Quick não, então por que preciso de tudo isso?
Na verdade, não me lembro de quais drogas, mas, por algum motivo, decidi voltar ao básico da computação gráfica. Portanto, nos últimos estágios da rasterização, todas as coordenadas da cena são transferidas para o NKU (Coordenadas de dispositivos normalizados; mais conhecido como NDC = Coordenadas de dispositivos normalizados). Sim, ouvi dizer que a saída do shader de vértice é na verdade um espaço de clipe e depois disso ainda há distorção afim, mas tudo isso é para uma representação tridimensional, e em 2D é sempre w = 1 e, portanto, podemos assumir que a saída imediatamente para a NDC.
OK, NDC, o que vem a seguir? Que, se a largura da sua janela for de 800 pixels, a coordenada NDC do centro do pixel zero será -1; a coordenada central do 799º é 1. Em resumo, ndcX = -1 + 2 * i / 799. Agora imagine que haja um retângulo de 100 a 300 e eu quero desenhar a cena inteira não em uma janela inteira, mas nela. Usando esse conhecimento fragmentário, vou contar ndcX100, ndcX300, depois jogá-los no vertex shader e, depois do padrão
gl_Position = matrix * position;
“enrole” gl_Position.x linearmente em [ndcX100; ndcX300]. Fazemos o mesmo para o componente vertical. Este truque permitirá gerar cenas em qualquer retângulo selecionado da cena. Com esse conhecimento, o exemplo gráfico começou a sofrer alterações. Você pode ver a paróquia aqui - gráfico ; todo o sal em shaders/line.vsh
.
SceneItem / Scene
Os três meses seguintes foram a redação de TK, resultou em 12 folhas de A4 =). Paralelamente, consideramos arquitetura. Eles pegaram o MVC ... é o MVP ... é o MVC / MVP hierárquico ... ou mesmo o PAC - tudo isso são convenções, uma boa decomposição é importante.
Em geral, preparamos um exemplo de cena. As fontes estão disponíveis aqui - SceneSample . Descobriu uma certa estrutura para criar aplicativos com gráficos no QtQML / Quick. Por favor, não esqueça que este código ainda funciona como um exemplo. Sim, já está meio pronto e parece mais ou menos limpo, mas não está pronto.
Cena é o jogador principal. Essa classe monitora suas coordenadas NDC e atualiza as matrizes correspondentes. SceneCamera é amigo íntimo dele. A próxima entidade que vale a pena mencionar é SceneItem. É inútil por si só, contém apenas alguma lógica básica; herdá-lo - como LineStrip - e implementar o que você precisa. Ao mesmo tempo, em updatePaintNode
você precisa usar derivados de SceneMaterial - FlatColorMaterial como referência. As demais entidades também fazem algo =), todo tipo de manipulador, ferramenta. Muitas das classes não são lançadas na QML e você não pode ficar sem esse C ++; você se lembra do que não está pronto?
A segunda dificuldade é que, se quisermos usar controles dentro de uma nova cena, não conseguiremos fazer isso. Pensamos, decidimos que não precisávamos disso e continuamos com calma o desenvolvimento.
Vantagens da abordagem:
- tudo é desenhado em uma coluna da cena;
- não corrigimos Qt - ainda é possível adicionar controles QML regulares ao palco para que a ordem z entre eles e as curvas (ou outro SceneItem) esteja correta;
- menor consumo de memória em comparação com outras abordagens.
Contras
- urbermachine complexo;
- é necessário conhecimento de OpenGL e GLSL;
- solução semi-acabada.
Obviamente, encontramos algumas dificuldades durante o desenvolvimento. Um deles foi
Bug com ordem z
Quando tentamos exibir uma cena com curvas, vimos essas fotos:


À primeira vista, não estava claro "fizemos tudo certo!" Supunha-se vagamente que o gl_Position.z estivesse de alguma forma errado, mas por que era difícil entender o porquê à noite. Não desistimos: vimos que o Qt corrige os shaders e adiciona o código para alterar gl_Position.z, pensávamos. Depois de um tempo, ocorreu-nos: confundimos os dados da matriz com a mudança em z, e o Qt transfere seus valores para eles! Assim, o valor item.z do QML é mapeado para z do OpenGL ( SceneMaterial.cpp +20 ):

Erro com clip: true
Uma vez em um bate-papo, uma equipe de negócios envia uma tela onde a linha esquerda da grade de coordenadas desapareceu.

Nossas perguntas e respostas atormentaram o programa e descobrimos etapas para uma reprodução estável: definimos a escala do monitor para não um múltiplo de 100% e as linhas “piscam”. Artyom sentou-se, pensou e descobriu que, quando clip: true
e o item são retangulares, o glScissor é usado, mas seus argumentos são coordenadas inteiras de pixels! Nos itens da QML, eles são reais e a rasterização da linha caiu para o pixel seguinte / anterior, e as tesouras foram cortadas para a corrente.
width: Math.round(metrics.width + leftPadding + 2 * rightPadding + 0.5)
a cena assim: width: Math.round(metrics.width + leftPadding + 2 * rightPadding + 0.5)
. Consequentemente, o item da cena deve sempre ter coordenadas inteiras para evitar esses artefatos.
Em conclusão, vou trazer KDPV

Obrigado a todos pela atenção!