OpenSceneGraph: Técnicas básicas de programação

imagem

1. Introdução


Este artigo focará não apenas nos gráficos, mas também na organização do aplicativo que o utiliza, levando em consideração as especificidades do mecanismo OpenSceneGraph e o software que ele fornece.

Não é segredo que a chave para o sucesso de qualquer produto de software é uma arquitetura bem projetada que forneça a capacidade de manter e expandir o código escrito. Nesse sentido, o mecanismo que estamos considerando está em um nível bastante alto, fornecendo ao desenvolvedor um kit de ferramentas muito amplo, proporcionando a construção de uma arquitetura modular flexível.

Este artigo é bastante longo e inclui uma visão geral das várias ferramentas e técnicas (padrões de design, se você desejar) fornecidas pelo mecanismo do desenvolvedor. Todas as seções do artigo são fornecidas com exemplos, cujo código pode ser obtido no meu repositório .

1. Analisando Opções da Linha de Comandos


No C / C ++, os parâmetros da linha de comando são passados ​​através dos argumentos para a função main (). Nos exemplos anteriores, marcamos cuidadosamente esses parâmetros como não utilizados, agora vamos usá-los para informar alguns dados ao programa quando ele for iniciado.

O OSG possui ferramentas de análise de linha de comando internas.

Crie o seguinte exemplo

Exemplo de linha de comando
main.h

#ifndef MAIN_H #define MAIN_H #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif // MAIN_H 

main.cpp

 #include "main.h" int main(int argc, char *argv[]) { osg::ArgumentParser args(&argc, argv); std::string filename; args.read("--model", filename); osg::ref_ptr<osg::Node> root = osgDB::readNodeFile(filename); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


Defina os parâmetros de inicialização do programa no QtCreator



Executando o programa para execução, obtemos o resultado (modelo de caminhão retirado do mesmo OpenSceneGraph-Data )



Agora vamos ver um exemplo linha por linha

 osg::ArgumentParser args(&argc, argv); 

cria uma instância da classe analisador de linha de comando osg :: ArgumentParser. Quando criado, o construtor de classe recebe os argumentos aceitos pela função main () do sistema operacional.

 std::string filename; args.read("--model", filename); 

analisamos os argumentos, ou seja, procuramos a chave “–model” entre eles, colocando seu valor na string filename. Assim, usando essa chave, transferimos o nome do arquivo com um modelo tridimensional para o programa. Em seguida, carregamos este modelo e o exibimos

 osg::ref_ptr<osg::Node> root = osgDB::readNodeFile(filename); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); 

O método read () da classe osg :: ArgumentParser possui muitas sobrecargas, permitindo que você leia não apenas valores de string da linha de comando, mas também números inteiros, números de ponto flutuante, vetores, etc. Por exemplo, você pode ler um determinado parâmetro do tipo float

 float size = 0.0f; args.read("--size", size); 

Se esse parâmetro não for exibido na linha de comando, seu valor permanecerá como estava após a inicialização da variável de tamanho.

2. Mecanismo de notificação e registro


O OpenSceneGraph possui um mecanismo de notificação que permite exibir mensagens de depuração durante o processo de renderização, bem como iniciado pelo desenvolvedor. Esta é uma grande ajuda ao rastrear e depurar um programa. O sistema de notificação OSG suporta a saída de informações de diagnóstico (erros, avisos, notificações) no nível do núcleo do mecanismo e seus plug-ins. O desenvolvedor pode exibir uma mensagem de diagnóstico durante a operação do programa usando a função osg :: notify ().

Esta função funciona como um fluxo de saída padrão da biblioteca C ++ padrão através da sobrecarga do operador <<. Ele leva o nível da mensagem como argumento: SEMPRE, FATAL, WARN, AVISO, INFORMAÇÃO, DEBUG_INFO e DEBUG_FP. Por exemplo

 osg::notify(osg::WARN) << "Some warning message" << std::endl; 

exibe um aviso com texto definido pelo usuário.

As notificações OSG podem conter informações importantes sobre o estado do programa, extensões do subsistema gráfico do computador, possíveis problemas com o mecanismo.

Em alguns casos, é necessário enviar esses dados não para o console, mas para poder redirecionar essa saída para um arquivo (na forma de um log) ou para qualquer outra interface, incluindo um widget gráfico. O mecanismo contém uma classe especial osg :: NotifyHandler que fornece redirecionamento de notificações para o fluxo de saída que o desenvolvedor precisa.

Usando um exemplo simples, considere como você pode redirecionar a saída de notificações, por exemplo, para um arquivo de log de texto. Escreva o seguinte código

Exemplo de notificação
main.h

 #ifndef MAIN_H #define MAIN_H #include <osgDB/ReadFile> #include <osgViewer/Viewer> #include <fstream> #endif // MAIN_H 

main.cpp

 #include "main.h" class LogFileHandler : public osg::NotifyHandler { public: LogFileHandler(const std::string &file) { _log.open(file.c_str()); } virtual ~LogFileHandler() { _log.close(); } virtual void notify(osg::NotifySeverity severity, const char *msg) { _log << msg; } protected: std::ofstream _log; }; int main(int argc, char *argv[]) { osg::setNotifyLevel(osg::INFO); osg::setNotifyHandler(new LogFileHandler("../logs/log.txt")); osg::ArgumentParser args(&argc, argv); osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(args); if (!root) { OSG_FATAL << args.getApplicationName() << ": No data loaded." << std::endl; return -1; } osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


Para redirecionar a saída, escrevemos a classe LogFileHandler, que é a sucessora de osg :: NotifyHandler. O construtor e destruidor desta classe controla a abertura e o fechamento do fluxo de saída _log ao qual o arquivo de texto está associado. O método notify () é um método de classe base semelhante que redefinimos para gerar as notificações de arquivo enviadas pelo OSG durante a operação através do parâmetro msg.

Classe LogFileHandler

 class LogFileHandler : public osg::NotifyHandler { public: LogFileHandler(const std::string &file) { _log.open(file.c_str()); } virtual ~LogFileHandler() { _log.close(); } virtual void notify(osg::NotifySeverity severity, const char *msg) { _log << msg; } protected: std::ofstream _log; }; 

Em seguida, no programa principal, execute as configurações necessárias

 osg::setNotifyLevel(osg::INFO); 

defina o nível de notificações INFO, ou seja, a saída para o log de todas as informações sobre a operação do mecanismo, incluindo as notificações atuais de operação normal.

 osg::setNotifyHandler(new LogFileHandler("../logs/log.txt")); 

instale o manipulador de notificações. Em seguida, processamos argumentos de linha de comando nos quais os caminhos para os modelos carregados são transmitidos

 osg::ArgumentParser args(&argc, argv); osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(args); if (!root) { OSG_FATAL << args.getApplicationName() << ": No data loaded." << std::endl; return -1; } 

Ao mesmo tempo, lidamos com a situação de falta de dados na linha de comando, exibindo uma mensagem no modo manual de logon usando a macro OSG_FATAL. Execute o programa com os seguintes argumentos



obtendo saída para um arquivo de log como este

Exemplo de log OSG
 Opened DynamicLibrary osgPlugins-3.7.0/mingw_osgdb_osgd.dll CullSettings::readEnvironmentalVariables() CullSettings::readEnvironmentalVariables() Opened DynamicLibrary osgPlugins-3.7.0/mingw_osgdb_deprecated_osgd.dll OSGReaderWriter wrappers loaded OK CullSettings::readEnvironmentalVariables() void StateSet::setGlobalDefaults() void StateSet::setGlobalDefaults() ShaderPipeline disabled. StateSet::setGlobalDefaults() Setting up GL2 compatible shaders CullSettings::readEnvironmentalVariables() CullSettings::readEnvironmentalVariables() CullSettings::readEnvironmentalVariables() CullSettings::readEnvironmentalVariables() ShaderComposer::ShaderComposer() 0xa5ce8f0 CullSettings::readEnvironmentalVariables() ShaderComposer::ShaderComposer() 0xa5ce330 View::setSceneData() Reusing existing scene0xa514220 CameraManipulator::computeHomePosition(0, 0) boundingSphere.center() = (-6.40034 1.96225 0.000795364) boundingSphere.radius() = 16.6002 CameraManipulator::computeHomePosition(0xa52f138, 0) boundingSphere.center() = (-6.40034 1.96225 0.000795364) boundingSphere.radius() = 16.6002 Viewer::realize() - No valid contexts found, setting up view across all screens. Applying osgViewer::ViewConfig : AcrossAllScreens . . . . ShaderComposer::~ShaderComposer() 0xa5ce330 ShaderComposer::~ShaderComposer() 0xa5ce8f0 ShaderComposer::~ShaderComposer() 0xa5d6228 close(0x1)0xa5d3e50 close(0)0xa5d3e50 ContextData::unregisterGraphicsContext 0xa5d3e50 DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. ShaderComposer::~ShaderComposer() 0xa5de4e0 close(0x1)0xa5ddba0 close(0)0xa5ddba0 ContextData::unregisterGraphicsContext 0xa5ddba0 Done destructing osg::View DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. Closing DynamicLibrary osgPlugins-3.7.0/mingw_osgdb_osgd.dll Closing DynamicLibrary osgPlugins-3.7.0/mingw_osgdb_deprecated_osgd.dll 


Não importa que, no momento, essas informações possam parecer inúteis para você - no futuro, essa conclusão poderá ajudar a depurar erros no seu programa.

Por padrão, o OSG envia mensagens para a saída padrão std :: cout e mensagens de erro para o fluxo std :: cerr. No entanto, substituindo o manipulador de notificação, conforme mostrado no exemplo, essa saída pode ser redirecionada para qualquer fluxo de saída, incluindo elementos da GUI.

Lembre-se de que, ao definir um nível alto de notificações (por exemplo, FATAL), o sistema ignora todas as notificações de nível inferior. Por exemplo, em um caso semelhante

 osg::setNotifyLevel(osg::FATAL); . . . osg::notify(osg::WARN) << "Some message." << std::endl; 

uma mensagem personalizada simplesmente não será exibida.

3. Intercepção de atributos geométricos


A classe osg :: Geometry gerencia um conjunto de dados que descreve vértices e exibe uma malha de polígono usando um conjunto ordenado de primitivas. No entanto, essa classe não tem idéia sobre elementos da topologia do modelo, como faces, arestas e o relacionamento entre elas. Essa nuance impede a implementação de coisas como mover determinadas faces, por exemplo, ao animar modelos. Atualmente, o OSG não suporta essa funcionalidade.

No entanto, o mecanismo implementa vários functores que permitem reler os atributos geométricos de qualquer objeto e usá-los para modelar a topologia da malha poligonal. No C ++, um functor é uma construção que permite usar um objeto como uma função.

A classe osg :: Drawable fornece ao desenvolvedor quatro tipos de functores:

  1. osg :: Drawable :: AttributeFunctor - lê os atributos dos vértices como uma matriz de ponteiros. Ele possui vários métodos virtuais para aplicar atributos de vértice de diferentes tipos de dados. Para usar este functor, você deve descrever a classe e substituir um ou mais de seus métodos, dentro dos quais as ações requeridas pelo desenvolvedor são executadas


 virtual void apply( osg::Drawable::AttributeType type, unsigned int size, osg::Vec3* ptr ) { //  3-     ptr. //      } 

  1. osg :: Drawable :: ConstAttributeFunctor - versão somente leitura do functor anterior: um ponteiro para uma matriz de vetores é passado como um parâmetro constante
  2. osg :: PrimitiveFunctor - imita o processo de renderização de objetos OpenGL. Sob o pretexto de renderizar um objeto, os métodos functor substituídos pelo desenvolvedor são chamados. Esse functor possui duas subclasses de modelo importantes: osg :: TemplatePrimitiveFunctor <> e osg :: TriangleFunctor <>. Essas classes recebem vértices primitivos como parâmetros e os transmitem aos métodos do usuário usando o operador operator ().
  3. osg :: PrimitiveIndexFunctor - executa as mesmas ações que o functor anterior, mas aceita os índices de vértice da primitiva como parâmetro.

Classes derivadas de osg :: Drawable, como osg :: ShapeDrawable e osg :: Geometry, têm um método accept () para aplicar vários functores.

4. Exemplo de uso do functor primitivo


Ilustramos a funcionalidade descrita usando o exemplo de coleta de informações sobre faces triangulares e pontos de alguma geometria que determinamos anteriormente.

Exemplo de Functor
main.h

 #ifndef MAIN_H #define MAIN_H #include <osg/Geode> #include <osg/Geometry> #include <osg/TriangleFunctor> #include <osgViewer/Viewer> #include <iostream> #endif 

main.cpp

 #include "main.h" std::string vec2str(const osg::Vec3 &v) { std::string tmp = std::to_string(vx()); tmp += " "; tmp += std::to_string(vy()); tmp += " "; tmp += std::to_string(vz()); return tmp; } struct FaceCollector { void operator()(const osg::Vec3 &v1, const osg::Vec3 &v2, const osg::Vec3 &v3) { std::cout << "Face vertices: " << vec2str(v1) << "; " << vec2str(v2) << "; " << vec2str(v3) << std::endl; } }; int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array; vertices->push_back( osg::Vec3(0.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(0.0f, 0.0f, 1.0f) ); vertices->push_back( osg::Vec3(1.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(1.0f, 0.0f, 1.5f) ); vertices->push_back( osg::Vec3(2.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(2.0f, 0.0f, 1.0f) ); vertices->push_back( osg::Vec3(3.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(3.0f, 0.0f, 1.5f) ); vertices->push_back( osg::Vec3(4.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(4.0f, 0.0f, 1.0f) ); osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array; normals->push_back( osg::Vec3(0.0f, -1.0f, 0.0f) ); osg::ref_ptr<osg::Geometry> geom = new osg::Geometry; geom->setVertexArray(vertices.get()); geom->setNormalArray(normals.get()); geom->setNormalBinding(osg::Geometry::BIND_OVERALL); geom->addPrimitiveSet(new osg::DrawArrays(GL_QUAD_STRIP, 0, 10)); osg::ref_ptr<osg::Geode> root = new osg::Geode; root->addDrawable(geom.get()); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); osg::TriangleFunctor<FaceCollector> functor; geom->accept(functor); return viewer.run(); } 


Omitindo o processo de criação de geometria considerada por nós muitas vezes, vamos prestar atenção ao seguinte. Definimos uma estrutura FaceCollector para a qual redefinimos operator () da seguinte maneira

 struct FaceCollector { void operator()(const osg::Vec3 &v1, const osg::Vec3 &v2, const osg::Vec3 &v3) { std::cout << "Face vertices: " << vec2str(v1) << "; " << vec2str(v2) << "; " << vec2str(v3) << std::endl; } }; 

Este operador, quando chamado, exibirá as coordenadas dos três vértices transmitidos a ele pelo mecanismo. A função vec2str é necessária para converter os componentes do vetor osg :: Vec3 em std :: string. Para chamar o functor, crie uma instância e passe-a para o objeto geometry através do método accept ()

 osg::TriangleFunctor<FaceCollector> functor; geom->accept(functor); 

Essa chamada, como mencionado acima, imita a renderização da geometria, substituindo o próprio desenho, chamando um método functor substituído. Nesse caso, ele será chamado durante o "desenho" de cada um dos triângulos que compõem a geometria do exemplo.

Na tela, temos essa geometria



e um escape para o console

 Face vertices: 0.000000 0.000000 0.000000; 0.000000 0.000000 1.000000; 1.000000 0.000000 0.000000 Face vertices: 0.000000 0.000000 1.000000; 1.000000 0.000000 1.500000; 1.000000 0.000000 0.000000 Face vertices: 1.000000 0.000000 0.000000; 1.000000 0.000000 1.500000; 2.000000 0.000000 0.000000 Face vertices: 1.000000 0.000000 1.500000; 2.000000 0.000000 1.000000; 2.000000 0.000000 0.000000 Face vertices: 2.000000 0.000000 0.000000; 2.000000 0.000000 1.000000; 3.000000 0.000000 0.000000 Face vertices: 2.000000 0.000000 1.000000; 3.000000 0.000000 1.500000; 3.000000 0.000000 0.000000 Face vertices: 3.000000 0.000000 0.000000; 3.000000 0.000000 1.500000; 4.000000 0.000000 0.000000 Face vertices: 3.000000 0.000000 1.500000; 4.000000 0.000000 1.000000; 4.000000 0.000000 0.000000 

De fato, ao chamar geom-> accept (...), triângulos não são renderizados, chamadas OpenGL são simuladas e, em vez deles, dados sobre os vértices do triângulo, cuja renderização é simulada



A classe osg :: TemplatePrimitiveFunctor coleta dados não apenas sobre triângulos, mas também sobre quaisquer outras primitivas do OpenGL. Para implementar o processamento desses dados, você deve substituir os seguintes operadores no argumento do modelo

 //   void operator()( const osg::Vec3&, bool ); //   void operator()( const osg::Vec3&, const osg::Vec3&, bool ); //   void operator()( const osg::Vec3&, const osg::Vec3&, const osg::Vec3&, bool ); //   void operator()( const osg::Vec3&, const osg::Vec3&, const osg::Vec3&, const osg::Vec3&, bool ); 


5. O Padrão do Visitante


O padrão de visitante é usado para acessar operações para alterar os elementos do gráfico de cena sem alterar as classes desses elementos. A classe de visitante implementa todas as funções virtuais relevantes para aplicá-las a vários tipos de elementos por meio de um mecanismo de despacho duplo. Usando esse mecanismo, o desenvolvedor pode criar sua própria instância de visitante implementando a funcionalidade necessária com a ajuda de operadores especiais e vinculando o visitante a vários tipos de elementos gráficos de cena em tempo real, sem alterar a funcionalidade dos próprios elementos. Essa é uma ótima maneira de estender a funcionalidade de um elemento sem definir subclasses desses elementos.

Para implementar esse mecanismo no OSG, a classe osg :: NodeVisitor é definida. A classe herdada de osg :: NodeVisitor se move pelo gráfico da cena, visita cada nó e aplica as operações definidas pelo desenvolvedor a ele. Essa é a classe principal usada para intervir no processo de atualização de nós e recortar nós invisíveis, além de aplicar outras operações relacionadas à modificação da geometria dos nós de cenas, como osgUtil :: SmoothingVisitor, osgUtil :: Simplifier e osgUtil :: TriStripVisitor.

Para subclassificar o visitante, devemos substituir um ou mais métodos apply () sobrecarregados virtuais fornecidos pela classe base osg :: NodeVisitor. A maioria dos principais tipos de nós OSG possui esses métodos. O visitante chamará automaticamente o método apply () para cada um dos nós visitados ao percorrer o gráfico da cena da cena. O desenvolvedor substitui o método apply () para cada um dos tipos de nós que ele precisa.

Na implementação do método apply (), o desenvolvedor, no momento apropriado, deve chamar o método traverse () da classe base osg :: NodeVisitor. Isso inicia a transição do visitante para o próximo nó, um filho ou um vizinho no nível da hierarquia, se o nó atual não tiver nenhum nó filho no qual a transição possa ser feita. A ausência de uma chamada para atravessar () significa interromper a travessia do gráfico de cena e o restante do gráfico de cena é ignorado.

Sobrecargas do método apply () têm formatos unificados

 virtual void apply( osg::Node& ); virtual void apply( osg::Geode& ); virtual void apply( osg::Group& ); virtual void apply( osg::Transform& ); 

Para ignorar o subgráfico do nó atual do objeto visitante, você deve definir o modo de rastreamento, por exemplo,

 ExampleVisitor visitor; visitor->setTraversalMode( osg::NodeVisitor::TRAVERSE_ALL_CHILDREN ); node->accept( visitor ); 

O modo de desvio é definido por vários enumeradores

  1. TRAVERSE_ALL_CHILDREN - movendo-se por todos os nós filhos.
  2. TRAVERSE_PARENTS - retorna do nó atual, não alcançando o nó raiz
  3. TRAVERSE_ACTIVE_CHILDREN - ignora nós exclusivamente ativos, ou seja, aqueles cuja visibilidade é ativada através do nó osg :: Switch.


6. Análise da estrutura do cessna em chamas


O desenvolvedor sempre pode analisar a parte do gráfico de cena gerada pelo modelo carregado no arquivo.

Exemplo de Functor
main.h

 #ifndef MAIN_H #define MAIN_H #include <osgDB/ReadFile> #include <osgViewer/Viewer> #include <iostream> #endif 

main.cpp

 #include "main.h" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class InfoVisitor : public osg::NodeVisitor { public: InfoVisitor() : _level(0) { setTraversalMode(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN); } std::string spaces() { return std::string(_level * 2, ' '); } virtual void apply(osg::Node &node); virtual void apply(osg::Geode &geode); protected: unsigned int _level; }; //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ void InfoVisitor::apply(osg::Node &node) { std::cout << spaces() << node.libraryName() << "::" << node.className() << std::endl; _level++; traverse(node); _level--; } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ void InfoVisitor::apply(osg::Geode &geode) { std::cout << spaces() << geode.libraryName() << "::" << geode.className() << std::endl; _level++; for (unsigned int i = 0; i < geode.getNumDrawables(); ++i) { osg::Drawable *drawable = geode.getDrawable(i); std::cout << spaces() << drawable->libraryName() << "::" << drawable->className() << std::endl; } traverse(geode); _level--; } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ int main(int argc, char *argv[]) { osg::ArgumentParser args(&argc, argv); osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(args); if (!root.valid()) { OSG_FATAL << args.getApplicationName() << ": No data leaded. " << std::endl; return -1; } InfoVisitor infoVisitor; root->accept(infoVisitor); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


Criamos a classe InfoVisitor, herdando-a de osg :: NodeVisitor

 class InfoVisitor : public osg::NodeVisitor { public: InfoVisitor() : _level(0) { setTraversalMode(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN); } std::string spaces() { return std::string(_level * 2, ' '); } virtual void apply(osg::Node &node); virtual void apply(osg::Geode &geode); protected: unsigned int _level; }; 

A propriedade protected_level apontará para o nível do gráfico de cena no qual nossa classe de visitantes está localizada atualmente. No construtor, inicialize o contador de nível e defina o modo de travessia do nó - para ignorar todos os nós filhos.

Agora redefina os métodos apply () para nós

 void InfoVisitor::apply(osg::Node &node) { std::cout << spaces() << node.libraryName() << "::" << node.className() << std::endl; _level++; traverse(node); _level--; } 

Aqui vamos mostrar o tipo do nó atual. O método libraryName () para o nó exibe o nome da biblioteca OSG em que esse nó está implementado e o método className exibe o nome da classe do nó. Esses métodos são implementados através do uso de macros no código das bibliotecas OSG.

 std::cout << spaces() << node.libraryName() << "::" << node.className() << std::endl; 

Depois disso, aumentamos o contador do nível do gráfico e chamamos o método traverse (), iniciando uma transição para um nível superior, para o nó filho. Após retornar de traverse (), diminuímos novamente o valor do contador. É fácil adivinhar que traverse () inicia uma chamada repetida para o método apply (), traverse repetido () já para um subgráfico iniciando no nó atual. Temos execução recursiva do visitante até atingirmos os nós finais do gráfico de cena.

Para um nó final do tipo osg :: Geode, sua sobrecarga do método apply () é substituída

 void InfoVisitor::apply(osg::Geode &geode) { std::cout << spaces() << geode.libraryName() << "::" << geode.className() << std::endl; _level++; for (unsigned int i = 0; i < geode.getNumDrawables(); ++i) { osg::Drawable *drawable = geode.getDrawable(i); std::cout << spaces() << drawable->libraryName() << "::" << drawable->className() << std::endl; } traverse(geode); _level--; } 

com código de trabalho semelhante, exceto que exibimos dados em todos os objetos geométricos anexados ao nó geométrico atual

 for (unsigned int i = 0; i < geode.getNumDrawables(); ++i) { osg::Drawable *drawable = geode.getDrawable(i); std::cout << spaces() << drawable->libraryName() << "::" << drawable->className() << std::endl; } 

Na função main (), processamos argumentos de linha de comando através dos quais passamos uma lista de modelos carregados na cena e formamos a cena

 osg::ArgumentParser args(&argc, argv); osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(args); if (!root.valid()) { OSG_FATAL << args.getApplicationName() << ": No data leaded. " << std::endl; return -1; } 

Ao mesmo tempo, processamos erros relacionados à ausência de nomes de arquivo de modelo na linha de comando. Agora criamos uma classe de visitante e a passamos para o gráfico de cena para executar

 InfoVisitor infoVisitor; root->accept(infoVisitor); 

A seguir, estão as etapas para iniciar o visualizador, o que já fizemos várias vezes. Depois de iniciar o programa com parâmetros

 $ visitor ../data/cessnafire.osg 

veremos a seguinte saída para o console

 osg::Group osg::MatrixTransform osg::Geode osg::Geometry osg::Geometry osg::MatrixTransform osgParticle::ModularEmitter osgParticle::ModularEmitter osgParticle::ParticleSystemUpdater osg::Geode osgParticle::ParticleSystem osgParticle::ParticleSystem osgParticle::ParticleSystem osgParticle::ParticleSystem 

De fato, temos uma árvore completa da cena carregada. Com licença, onde estão tantos nós? Tudo é muito simples - os modelos do formato * .osg são contêineres que armazenam não apenas dados sobre a geometria do modelo, mas também outras informações sobre sua estrutura como um subgrafo da cena OSG. A geometria do modelo, transformações, efeitos de partículas que realizam fumaça e chama são todos nós do gráfico de cena OSG.Qualquer cena pode ser baixada de * .osg ou descarregada do visualizador para o formato * .osg.

Este é um exemplo simples de aplicação da mecânica do visitante. De fato, dentro dos visitantes, você pode executar muitas operações para modificar os nós quando o programa está sendo executado.

7. Controlando o comportamento dos nós no gráfico de cena, substituindo o método traverse ()


Uma maneira importante de trabalhar com o OSG é substituir o método traverse (). Esse método é chamado toda vez que um quadro é desenhado. Eles aceitam um parâmetro do tipo osg :: NodeVisitor e que informa qual passagem do gráfico de cena está sendo executada atualmente (atualização, processamento de eventos ou recorte). A maioria dos hosts OSG substitui esse método para implementar sua funcionalidade.

Deve-se lembrar que a substituição do método traverse () pode ser perigosa, pois afeta o processo de atravessar o gráfico da cena e pode levar à exibição incorreta da cena. Também é inconveniente se você deseja adicionar novas funcionalidades a vários tipos de nós. Nesse caso, são usados ​​retornos de chamada de nó, cuja conversa será um pouco menor.

Já sabemos que o nó osg :: Switch pode controlar a exibição de seus nós filhos, incluindo a exibição de alguns nós e desativando a exibição de outros. Mas como ele não sabe fazer isso automaticamente, criaremos um novo nó baseado no antigo, que alternará entre nós filhos em diferentes momentos no tempo, de acordo com o valor do contador interno.

Exemplo de animswitch
main.h

 #ifndef MAIN_H #define MAIN_H #include <osg/Switch> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif 

main.cpp

 #include "main.h" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class AnimatingSwitch : public osg::Switch { public: AnimatingSwitch() : osg::Switch(), _count(0) {} AnimatingSwitch(const AnimatingSwitch &copy, const osg::CopyOp &copyop = osg::CopyOp::SHALLOW_COPY) : osg::Switch(copy, copyop), _count(copy._count) {} META_Node(osg, AnimatingSwitch); virtual void traverse(osg::NodeVisitor &nv); protected: unsigned int _count; }; void AnimatingSwitch::traverse(osg::NodeVisitor &nv) { if (!((++_count) % 60) ) { setValue(0, !getValue(0)); setValue(1, !getValue(1)); } osg::Switch::traverse(nv); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ 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<AnimatingSwitch> root = new AnimatingSwitch; root->addChild(model1.get(), true); root->addChild(model2.get(), false); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


Vamos dar uma olhada neste exemplo. Estamos criando uma nova classe AnimatingSwitch que herda de osg :: Switch.

 class AnimatingSwitch : public osg::Switch { public: AnimatingSwitch() : osg::Switch(), _count(0) {} AnimatingSwitch(const AnimatingSwitch &copy, const osg::CopyOp &copyop = osg::CopyOp::SHALLOW_COPY) : osg::Switch(copy, copyop), _count(copy._count) {} META_Node(osg, AnimatingSwitch); virtual void traverse(osg::NodeVisitor &nv); protected: unsigned int _count; }; void AnimatingSwitch::traverse(osg::NodeVisitor &nv) { if (!((++_count) % 60) ) { setValue(0, !getValue(0)); setValue(1, !getValue(1)); } osg::Switch::traverse(nv); } 

Esta classe contém o construtor padrão.

 AnimatingSwitch() : osg::Switch(), _count(0) {} 

e construtor para cópia, criado de acordo com os requisitos da OSG

 AnimatingSwitch(const AnimatingSwitch &copy, const osg::CopyOp &copyop = osg::CopyOp::SHALLOW_COPY) : osg::Switch(copy, copyop), _count(copy._count) {} 

O construtor para copiar deve conter como parâmetros: uma referência constante à instância da classe a ser copiada e o parâmetro osg :: CopyOp que especifica as configurações de cópia da classe. Cartas muito estranhas seguem

 META_Node(osg, AnimatingSwitch); 

Essa é uma macro que forma a estrutura necessária para o descendente de uma classe derivada de osg :: Node. Até atribuirmos importância a essa macro, é importante que ela esteja presente ao herdar de osg :: Switch ao definir todas as classes descendentes. A classe contém o campo protegido _count - o próprio contador com base no qual alternamos. Implementamos a alternância ao substituir o método traverse ()

 void AnimatingSwitch::traverse(osg::NodeVisitor &nv) { if (!((++_count) % 60) ) { setValue(0, !getValue(0)); setValue(1, !getValue(1)); } osg::Switch::traverse(nv); } 

A mudança do status de exibição dos nós ocorrerá toda vez que o valor do contador (aumentando cada chamada de método) for múltiplo de 60. Compilamos o exemplo e o executamos



Como o método traverse () é constantemente redefinido para vários tipos de nós, ele deve fornecer um mecanismo para obter matrizes de transformação e renderizar estados para uso posterior por seu algoritmo sobrecarregado. O parâmetro de entrada osg :: NodeVisitor é a chave para várias operações com nós. Em particular, indica o tipo de passagem atual do gráfico de cena, como atualização, processamento de eventos e recorte de faces invisíveis. Os dois primeiros estão relacionados a retornos de chamada de nó e serão considerados ao estudar animação.

A passagem de recorte pode ser identificada convertendo o objeto osg :: NodeVisitor em osg :: CullVisitor

 osgUtil::CullVisitor *cv = dynamic_cast<osgUtil::CullVisitor *>(&nv); if (cv) { ///  - ,     } 


8. Mecanismo de retorno de chamada


No artigo anterior, implementamos a animação de um objeto de cena alterando os parâmetros de sua transformação dentro do ciclo de renderização da cena. Como já foi mencionado muitas vezes, essa abordagem contém um comportamento potencialmente perigoso do aplicativo na renderização multithread. Para resolver esse problema, é usado um mecanismo de retorno de chamada que é executado ao percorrer o gráfico de cena.

Existem vários tipos de retornos de chamada no mecanismo. Os retornos de chamada são implementados por classes especiais, entre as quais osg :: NodeCallback é projetado para lidar com o processo de atualização de nós de cena, e osg :: Drawable :: UpdateCallback, osg :: Drawable :: EventCallback e osg :: Drawable: CullCallback - executam as mesmas funções, mas para objetos de geometria.

A classe osg :: NodeCallback possui um operador operator (substituível virtual) fornecido pelo desenvolvedor para implementar sua própria funcionalidade. Para que o retorno de chamada funcione, você deve anexar uma instância da classe de chamada ao nó para o qual será processado chamando o método setUpdateCallback () ou addUpdateCallback (). O operador operator () é chamado automaticamente durante a atualização dos nós no gráfico de cena ao renderizar cada quadro.

A tabela a seguir lista os retornos de chamada disponíveis para o desenvolvedor no OSG.

Primeiro nomeFunção de retorno de chamadaMétodo virtualMétodo para anexar a um objeto
Atualização do nóosg :: NodeCallbackoperador ()osg :: Node :: setUpdateCallback ()
Evento do nóosg :: NodeCallbackoperador ()osg :: Node :: setEventCallback ()
Recorte de nóosg :: NodeCallbackoperador ()osg :: Node :: setCullCallback ()
Atualização de geometriaosg::Drawable::UpdateCallbackupdate()osg::Drawable::setUpdateCallback()
osg::Drawable::EventCallbackevent()osg::Drawable::setEventCallback()
osg::Drawable::CullCallbackcull()osg::Drawable::setCullCallback()
osg::StateAttributeCallbackoperator()osg::StateAttribute::setUpdateCallback()
osg::StateAttributeCallbackoperator()osg::StateAttribute::setEventCallback()
osg::Uniform::Callbackoperator()osg::Uniform::setUpdateCallback()
osg::Uniform::Callbackoperator()osg::Uniform::setEvevtCallback()
osg::Camera::DrawCallbackoperator()osg::Camera::PreDrawCallback()
osg::Camera::DrawCallbackoperator()osg::Camera::PostDrawCallback()


9. osg::Switch


Um pouco mais alto, escrevemos um exemplo com a troca de dois modelos de aeronaves. Agora repetiremos este exemplo, mas faremos tudo corretamente usando o mecanismo de retorno de chamada OSG.

Retorno de chamada com exemplo
main.h

 #ifndef MAIN_H #define MAIN_H #include <osg/Switch> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif 

main.cpp

 #include "main.h" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class SwitchingCallback : public osg::NodeCallback { public: SwitchingCallback() : _count(0) {} virtual void operator()(osg::Node *node, osg::NodeVisitor *nv); protected: unsigned int _count; }; //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ void SwitchingCallback::operator()(osg::Node *node, osg::NodeVisitor *nv) { osg::Switch *switchNode = static_cast<osg::Switch *>(node); if ( !((++_count) % 60) && switchNode ) { switchNode->setValue(0, !switchNode->getValue(0)); switchNode->setValue(1, !switchNode->getValue(0)); } traverse(node, nv); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ 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, true); root->addChild(model2, false); root->setUpdateCallback( new SwitchingCallback ); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


Você deve criar uma classe herdada de osg :: NodeCallback, que controla o nó osg :: Switch

 class SwitchingCallback : public osg::NodeCallback { public: SwitchingCallback() : _count(0) {} virtual void operator()(osg::Node *node, osg::NodeVisitor *nv); protected: unsigned int _count; }; 

O contador _count controlará a alternância do nó osg :: Switch do mapeamento de um nó filho para outro, dependendo do seu valor. No construtor, inicializamos o contador e redefinimos o método virtual operator ()

 void SwitchingCallback::operator()(osg::Node *node, osg::NodeVisitor *nv) { osg::Switch *switchNode = static_cast<osg::Switch *>(node); if ( !((++_count) % 60) && switchNode ) { switchNode->setValue(0, !switchNode->getValue(0)); switchNode->setValue(1, !switchNode->getValue(0)); } traverse(node, nv); } 

O nó no qual a chamada trabalhou é passado através do parâmetro node. Como sabemos com certeza que este será um nó do tipo osg :: Switch, executamos uma conversão estática do ponteiro para o nó no ponteiro para o nó do switch

 osg::Switch *switchNode = static_cast<osg::Switch *>(node); 

Trocaremos os nós filhos exibidos com o valor válido desse ponteiro e, quando o valor do contador for múltiplo de 60

 if ( !((++_count) % 60) && switchNode ) { switchNode->setValue(0, !switchNode->getValue(0)); switchNode->setValue(1, !switchNode->getValue(0)); } 

Não se esqueça de chamar o método traverse () para continuar a travessia recursiva do gráfico de cena

 traverse(node, nv); 

O restante do código do programa é trivial, exceto a linha

 root->setUpdateCallback( new SwitchingCallback ); 

onde atribuímos o retorno de chamada que criamos ao nó raiz do tipo osg :: Switch. O programa funciona de maneira semelhante ao exemplo anterior



Até o momento, usamos o método misterioso traverse () para dois propósitos: substituir esse método nas classes sucessoras e chamar esse método na classe osg :: NodeVisitor para continuar percorrendo o gráfico da cena.

No exemplo que acabamos de examinar, usamos a terceira opção de chamar traverse (), passando um ponteiro para o nó e um ponteiro para a instância do visitante como parâmetros. Como nos dois primeiros casos, se não houver chamada para atravessar () neste nó, o rastreamento do gráfico da cena será interrompido.

Os métodos addUpdateCallback () também servem para adicionar um retorno de chamada ao nó. Ao contrário de setUpdateCallback (), ele é usado para adicionar outro retorno de chamada aos existentes. Portanto, pode haver vários retornos de chamada para o mesmo nó.

Conclusão


Examinamos as técnicas básicas usadas no desenvolvimento de aplicativos usando o mecanismo gráfico OpenSceneGraph. No entanto, isso está longe de ser todos os pontos que eu gostaria de abordar (apesar do fato de o artigo ter sido bastante longo), portanto,

Para continuar ...

Source: https://habr.com/ru/post/pt437688/


All Articles