
1. Introdução
Quando um ponto, linha ou polígono complexo é desenhado em um mundo tridimensional, o resultado final será exibido em uma tela plana e bidimensional. Assim, objetos tridimensionais passam por um determinado caminho de transformação, transformando-se em um conjunto de pixels exibidos em uma janela bidimensional.
O desenvolvimento de ferramentas de software que implementam gráficos tridimensionais chegou, independentemente de qual você escolher, aproximadamente ao mesmo conceito de descrições matemáticas e algorítmicas das transformações acima. Ideologicamente, APIs gráficas "limpas", como o OpenGL, e mecanismos de jogos legais, como Unity e Unreal, usam mecanismos semelhantes para descrever a transformação de uma cena tridimensional. OpenSceneGraph não é excepção.
Neste artigo, revisaremos os mecanismos para agrupar e transformar objetos tridimensionais no OSG.
1. Matriz modelo, matriz de exibição e matriz de projeção
Três matrizes básicas envolvidas na transformação de coordenadas estão envolvidas na transformação entre diferentes sistemas de coordenadas. Freqüentemente, em termos de OpenGL, eles são chamados de
matriz modelo , matriz de exibição e
matriz de projeção .
A matriz do modelo é usada para descrever a localização do objeto no mundo 3D. Ele converte vértices do
sistema de coordenadas local do objeto para o
sistema de coordenadas do
mundo . A propósito, todos os sistemas de coordenadas no OSG são
destros .
O próximo passo é a transformação das coordenadas do mundo em um espaço de visualização, realizado usando a matriz de visualização. Suponha que tenhamos uma câmera localizada na origem do sistema de coordenadas do mundo. A matriz inversa à matriz de transformação da câmera é realmente usada como uma matriz de visualização. Em um sistema de coordenadas destro, o OpenGL, por padrão, sempre define uma câmera localizada no ponto (0, 0, 0) do sistema de coordenadas global e direcionada ao longo da direção negativa do eixo Z.
Noto que no OpenGL a matriz do modelo e a matriz da vista não são separadas. No entanto, a matriz de visualização de modelo é determinada lá, que converte as coordenadas locais do objeto nas coordenadas do espaço de visualização. Essa matriz, de fato, é o produto da matriz do modelo e da matriz do formulário. Assim, a transformação de um vértice V de coordenadas locais em um espaço da forma pode ser escrita condicionalmente como o produto
Ve = V * modelViewMatrix
A próxima tarefa importante é determinar como os objetos 3D serão projetados no plano da tela e calcular a chamada
pirâmide de recorte - uma área do espaço que contém objetos a serem exibidos na tela. A matriz de projeção é usada para especificar a pirâmide de recorte definida no espaço mundial por seis planos: esquerdo, direito, inferior, superior, próximo e distante. O OpenGL fornece a função gluPerapective (), que permite especificar uma pirâmide de recorte e uma maneira de projetar um mundo tridimensional em um plano.
O sistema de coordenadas obtido após as transformações acima é chamado de
sistema de coordenadas normalizadas do dispositivo , possui um intervalo de coordenadas de -1 a 1 em cada eixo e é canhoto. E, como último passo, os dados recebidos são projetados na porta de exibição (janela de visualização) da janela, definida pelo retângulo da área do cliente da janela. Depois disso, o mundo 3D aparece na nossa tela 2D. O valor final das coordenadas da tela dos vértices Vs pode ser expresso pela seguinte transformação
Vs = V * modelViewMatrix * projectionMatrix * windowMatrix
ou
Vs = V * MVPW
em que MVPW é a matriz de transformação equivalente igual ao produto de três matrizes: matrizes de visualização de modelo, matrizes de projeção e matrizes de janela.
Vs nessa situação é um vetor tridimensional que determina a posição de um pixel 2D com um valor de profundidade. Invertendo a operação de transformação de coordenadas, obtemos uma linha no espaço tridimensional. Portanto, um ponto 2D pode ser considerado como dois pontos - um no próximo (Zs = 0) e outro no plano de recorte distante (Zs = 1). As coordenadas desses pontos no espaço tridimensional
V0 = (Xs, Ys, 0) * invMVPW V1 = (Xs, Ys, 1) * invMVPW
onde invMVPW é o inverso do MVPW.
Em todos os exemplos discutidos até o momento, criamos um único objeto tridimensional nas cenas. Nestes exemplos, as coordenadas locais do objeto sempre coincidiam com as coordenadas globais globais. Agora é hora de falar sobre ferramentas que permitem colocar muitos objetos na cena e mudar sua posição no espaço.
2. Nós do grupo
A classe osg :: Group é o chamado
nó do
grupo de um gráfico de cena no OSG. Pode ter qualquer número de nós filhos, incluindo nós de folha geométrica ou outros nós de grupo. Esses são os nós mais usados com ampla funcionalidade.
A classe osg :: Group é derivada da classe osg :: Node e, consequentemente, herda da classe osg :: Referenced. osg :: Group contém uma lista de nós filhos, em que cada nó filho é controlado por um ponteiro inteligente. Isso garante que não haja vazamentos de memória ao fazer cascata em um galho de uma árvore de cena. Esta classe fornece ao desenvolvedor vários métodos públicos.
- addChild () - acrescenta o nó ao final da lista de nós filhos. Por outro lado, existe o método insertChild (), que coloca o nó filho em uma posição específica na lista, especificado por um índice inteiro ou um ponteiro para o nó, passado como parâmetro.
- removeChild () e removeChildren () - remova um único nó ou grupo de nós.
- getChild () - obtendo um ponteiro para um nó pelo seu índice na lista
- getNumChildren () - obtendo o número de nós filhos anexados a este grupo.
Gerenciamento do nó pai
Como já sabemos, a classe osg :: Group gerencia grupos de seus objetos filhos, entre os quais podem haver instâncias osg :: Geode que controlam a geometria dos objetos de cena. Ambas as classes têm uma interface para gerenciar nós-pai.
O OSG permite que os nós da cena tenham vários nós principais (falaremos sobre isso mais tarde). Enquanto isso, veremos os métodos definidos em osg :: Node que são usados para manipular nós-pai:
- getParent () - retorna um ponteiro do tipo osg :: Group que contém uma lista de nós pais.
- getNumParants () - retorna o número de nós pais.
- getParentalNodePath () - retorna todos os caminhos possíveis para o nó raiz da cena a partir do nó atual. Retorna uma lista de variáveis do tipo osg :: NodePath.
osg :: NodePath é um vetor std :: de ponteiros para nós de cena.

Por exemplo, para a cena mostrada na figura, o seguinte código
osg::NodePath &nodePath = child3->getParentalNodePaths()[0]; for (unsigned int i = 0; i < nodePath.size(); ++i) { osg::Node *node = nodePath[i];
retornará nós Raiz, Filho1, Filho2.
Você não deve usar mecanismos de gerenciamento de memória para referenciar nós-pai. Quando um nó pai é excluído, todos os nós filhos são excluídos automaticamente, o que pode levar a uma falha no aplicativo.
3. Adicionando vários modelos à árvore de cenas
Ilustramos o mecanismo para usar grupos com o exemplo a seguir.
Exemplo de grupo completomain.h #ifndef MAIN_H #define MAIN_H #include <osg/Group> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif
main.cpp #include "main.h" int main(int argc, char *argv[]) { (void) argc, (void) argv; osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile("../data/cow.osg"); osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(model1.get()); root->addChild(model2.get()); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); }
Fundamentalmente, o exemplo difere de todos os anteriores porque carregamos dois modelos tridimensionais e, para adicioná-los à cena, criamos um nó raiz do grupo e adicionamos nossos modelos a ele como nós filhos
osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(model1.get()); root->addChild(model2.get());

Como resultado, temos uma cena composta por dois modelos - um avião e uma vaca-espelho engraçada. A propósito, uma vaca espelhada não será espelhada, a menos que você copie sua textura do OpenSceneGraph-Data / Images / reflect.rgb para o diretório data / Images do nosso projeto.
A classe osg :: Group pode aceitar qualquer tipo de nó como filho, incluindo nós desse tipo. Pelo contrário, a classe osg :: Geode não contém nenhum nó filho - é um nó terminal que contém a geometria do objeto de cena. Esse fato é conveniente quando se pergunta se o nó é do tipo osg :: Group ou outro tipo de derivado de osg :: Node. Vejamos um pequeno exemplo.
osg::ref_ptr<osg::Group> model = dynamic_cast<osg::Group *>(osgDB::readNodeFile("../data/cessna.osg"));
O valor retornado pela função osgDB :: readNodeFile () é sempre do tipo osg :: Node *, mas pode ser convertido em seu descendente osg :: Group *. Se o nó do modelo Cessna for um nó de grupo, a conversão será bem-sucedida; caso contrário, a conversão retornará NULL.
Você também pode executar o mesmo truque que funciona na maioria dos compiladores
Em locais de código com desempenho crítico, é melhor usar métodos de conversão especiais
osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("cessna.osg"); osg::Group* convModel1 = model->asGroup();
4. Nós de transformação
Nós Osg :: Group não podem fazer nenhuma transformação, exceto a capacidade de ir para seus nós filhos. O OSG fornece a classe osg :: Transform para o movimento da geometria espacial. Essa classe é sucessora da classe osg :: Group, mas também é abstrata - na prática, seus herdeiros são usados, os quais implementam várias transformações espaciais da geometria. Ao percorrer o gráfico de cena, o nó osg :: Transform adiciona sua transformação à matriz de transformação atual do OpenGL. Isso é equivalente à multiplicação das matrizes de transformação OpenGL pelo comando glMultMatrix ()

Este exemplo de gráfico de cena pode ser traduzido no seguinte código OpenGL
glPushMatrix(); glMultMatrix( matrixOfTransform1 ); renderGeode1(); glPushMatrix(); glMultMatrix( matrixOfTransform2 ); renderGeode2(); glPopMatrix(); glPopMatrix();
Podemos dizer que a posição de Geode1 é definida no sistema de coordenadas Transform1, e a posição de Geode2 é definida no sistema de coordenadas Transform2, deslocada em relação à Transform1. Ao mesmo tempo, o posicionamento em coordenadas absolutas pode ser ativado no OSG, o que levará ao comportamento do objeto, equivalente ao resultado do comando OpenGL glGlobalMatrix ()
transformNode->setReferenceFrame( osg::Transform::ABSOLUTE_RF );
Você pode voltar ao modo de posicionamento relativo
transformNode->setReferenceFrame( osg::Transform::RELATIVE_RF );
5. O conceito de matriz de transformação de coordenadas
O tipo osg :: Matrix é um tipo OSG básico não controlado por ponteiros inteligentes. Ele fornece uma interface para operações em matrizes 4x4 que descrevem a transformação de coordenadas, como mover, girar, dimensionar e calcular projeções. A matriz pode ser especificada explicitamente.
A classe osg :: Matrix fornece os seguintes métodos públicos:
- postMult () e operator * () - a multiplicação correta da matriz atual pela matriz ou pelo vetor passado como parâmetro. O método preMult () executa a multiplicação à esquerda.
- makeTranslate (), makeRotate () e makeScale () - redefine a matriz atual e crie uma matriz 4x4 que descreva o movimento, a rotação e a escala. suas versões estáticas translate (), rotate () e scale () podem ser usadas para criar um objeto de matriz com parâmetros específicos.
- invert () - calcula o inverso da matriz atual. Sua versão estática de inverse () pega uma matriz como parâmetro e retorna uma nova matriz inversa à determinada.
OSG entende matrizes como matrizes de strings e vetores como strings, portanto, para aplicar uma transformação de matriz em um vetor, faça isso
osg::Matrix mat = …; osg::Vec3 vec = …; osg::Vec3 resultVec = vec * mat;
A ordem das operações da matriz é fácil de entender, observando como as matrizes são multiplicadas para obter uma conversão equivalente
osg::Matrix mat1 = osg::Matrix::scale(sx, sy, sz); osg::Matrix mat2 = osg::Matrix::translate(x, y, z); osg::Matrix resultMat = mat1 * mat2;
O desenvolvedor deve ler o processo de transformação da esquerda para a direita. Ou seja, no fragmento de código descrito, o vetor primeiro escala e depois seu movimento.
osg :: Matrixf contém elementos do tipo float.
6. Usando a classe osg :: MatrixTransform
Aplicamos o conhecimento teórico adquirido na prática, carregando dois modelos de aeronaves em diferentes pontos da cena.
Texto completo do exemplo de transformaçãomain.h #ifndef MAIN_H #define MAIN_H #include <osg/MatrixTransform> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif
main.cpp #include "main.h" int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::MatrixTransform> transform1 = new osg::MatrixTransform; transform1->setMatrix(osg::Matrix::translate(-25.0, 0.0, 0.0)); transform1->addChild(model.get()); osg::ref_ptr<osg::MatrixTransform> transform2 = new osg::MatrixTransform; transform2->setMatrix(osg::Matrix::translate(25.0, 0.0, 0.0)); transform2->addChild(model.get()); osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(transform1.get()); root->addChild(transform2.get()); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); }
O exemplo é realmente bastante trivial. Carregando o modelo do avião a partir do arquivo
osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/cessna.osg");
Crie um nó de transformação
osg::ref_ptr<osg::MatrixTransform> transform1 = new osg::MatrixTransform;
Definimos o modelo para movê-lo ao longo das 25 unidades do eixo X para a esquerda como matriz de transformação
transform1->setMatrix(osg::Matrix::translate(-25.0, 0.0, 0.0));
Definimos nosso modelo para o nó de transformação como um nó filho
transform1->addChild(model.get());
Fazemos o mesmo com a segunda transformação, mas como matriz, definimos o movimento para a direita em 25 unidades
osg::ref_ptr<osg::MatrixTransform> transform2 = new osg::MatrixTransform; transform2->setMatrix(osg::Matrix::translate(25.0, 0.0, 0.0)); transform2->addChild(model.get());
Criamos um nó raiz e, como nós de transformação, definimos nós de transformação transform1 e transform2
osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(transform1.get()); root->addChild(transform2.get());
Crie um visualizador e passe o nó raiz para ele como dados da cena
osgViewer::Viewer viewer; viewer.setSceneData(root.get());
A execução do programa fornece uma imagem

A estrutura do gráfico de cena neste exemplo é a seguinte.

Não devemos ficar confusos pelo fato de os nós de transformação (Filho 1.1 e Filho 1.2) se referirem ao mesmo objeto filho do modelo de avião (Filho 2). Esse é um mecanismo OSG comum, quando um nó filho de um gráfico de cena pode ter vários nós pais. Portanto, não precisamos armazenar duas instâncias do modelo em nossa memória para obter dois planos idênticos em cena. Esse mecanismo permite alocar memória de maneira muito eficiente no aplicativo. O modelo não será excluído da memória até que seja referido como filho, pelo menos um nó.
Em sua ação, a classe osg :: MatrixTransform é equivalente aos comandos OpenGL glMultMatrix () e glLoadMatrix (), implementa todos os tipos de transformações espaciais, mas é difícil de usar devido à necessidade de calcular a matriz de transformação.
A classe osg :: PositionAttitudeTransform funciona como as funções OpenGL glTranslate (), glScale (), glRotate (). Ele fornece métodos públicos para converter nós filhos:
- setPosition () - move o nó para um determinado ponto no espaço especificado pelo parâmetro osg :: Vec3
- setScale () - dimensiona o objeto ao longo dos eixos de coordenadas. Os fatores de escala ao longo dos eixos correspondentes são definidos por um parâmetro do tipo osg :: Vec3
- setAttitude () - define a orientação espacial do objeto. Como parâmetro, usa o quaternion de conversão de rotação osg :: Quat, cujo construtor possui várias sobrecargas que permitem especificar o quaternion diretamente (no sentido dos componentes) e, por exemplo, através dos ângulos de Euler osg :: Quat (xAngle, osg :: X_AXIS, yAngle, osg :: Y_AXIS, zAngle, osg :: Z_AXIS) (os ângulos são dados em radianos!)
7. Nós do comutador
Considere outra classe - osg :: Switch, que permite exibir ou pular a renderização de um nó de cena, dependendo de alguma condição lógica. É um descendente da classe osg :: Group e atribui algum valor lógico a cada um de seus filhos. Possui vários métodos públicos úteis:
- O addChild () sobrecarregado, como um segundo parâmetro, utiliza uma chave lógica que indica se deve ou não exibir este nó.
- setValue () - define a chave de visibilidade / invisibilidade. Leva o índice do nó filho de seu interesse e o valor da chave desejada. Assim, getValue () permite que você obtenha o valor atual da chave pelo índice do nó de seu interesse.
- setNewChildDefaultValue () - define o valor padrão para a chave de visibilidade de todos os novos objetos adicionados como filhos.
Considere a aplicação desta classe com um exemplo.
Troca de exemplo completomain.h #ifndef MAIN_H #define MAIN_H #include <osg/Switch> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif
main.cpp #include "main.h" int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile("../data/cessnafire.osg"); osg::ref_ptr<osg::Switch> root = new osg::Switch; root->addChild(model1.get(), false); root->addChild(model2.get(), true); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); }
O exemplo é trivial - carregamos dois modelos: uma cessna convencional e uma cessna com o efeito de um motor em chamas
osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile("../data/cessnafire.osg");
No entanto, criamos osg :: Switch como o nó raiz, o que nos permite, ao adicionar modelos a ele como nós filhos, definir a chave de visibilidade para cada um deles
osg::ref_ptr<osg::Switch> root = new osg::Switch; root->addChild(model1.get(), false); root->addChild(model2.get(), true);
Ou seja, model1 não será renderizado e model2 será, o que observaremos executando o programa

Ao trocar os valores das teclas, veremos a imagem oposta
root->addChild(model1.get(), true); root->addChild(model2.get(), false);

Armando ambas as teclas, veremos dois modelos ao mesmo tempo
root->addChild(model1.get(), true); root->addChild(model2.get(), true);

Você pode ativar a visibilidade e a invisibilidade de um nó, filho de osg :: Switch, em movimento, usando o método setValue ()
switchNode->setValue(0, false); switchNode->setValue(0, true); switchNode->setValue(1, true); switchNode->setValue(1, false);
Conclusão
Neste tutorial, examinamos todas as principais classes de nós intermediários usadas no OpenSceeneGraph. Assim, lançamos mais um tijolo básico na base do conhecimento sobre o dispositivo desse mecanismo gráfico indubitavelmente interessante. Os exemplos discutidos no artigo, como antes,
estão disponíveis no meu repositório no Github .
Para continuar ...