OpenSceneGraph: Gráfico de cena e ponteiros inteligentes

imagem

1. Introdução


Em um artigo anterior, vimos o assembly OpenSceneGraph da fonte e escrevemos um exemplo elementar no qual um plano cinza fica suspenso em um mundo roxo vazio. Eu concordo, não muito impressionante. No entanto, como eu disse anteriormente, neste pequeno exemplo, existem os principais conceitos nos quais esse mecanismo gráfico se baseia. Vamos considerá-los com mais detalhes. O material abaixo usa ilustrações do blog de Alexander Bobkov sobre OSG (é uma pena que o autor tenha deixado de escrever sobre OSG ...). O artigo também é baseado em material e exemplos do livro OpenSceneGraph 3.0. Guia para iniciantes

Devo dizer que a publicação anterior foi sujeita a algumas críticas, com as quais concordo parcialmente - o material saiu não dito e foi retirado de contexto. Vou tentar corrigir essa omissão sob o corte.

1. Brevemente sobre o gráfico da cena e seus nós


O conceito central do mecanismo é o chamado gráfico de cena (não é coincidência que tenha ficado preso no nome da estrutura) - uma estrutura hierárquica em árvore que permite organizar uma representação lógica e espacial de uma cena tridimensional. O gráfico da cena contém o nó raiz e seus nós ou nós intermediários e terminais associados.

Por exemplo



Este gráfico mostra uma cena que consiste em uma casa e uma tabela nela. A casa tem uma certa representação geométrica e está localizada de uma certa maneira no espaço em relação a um determinado sistema de coordenadas básico associado ao nó raiz (raiz). A tabela também é descrita por alguma geometria, localizada de alguma forma em relação à casa e junto com a casa - em relação ao nó raiz. Todos os nós, com uma propriedade comum, porque herdam de uma classe osg :: Node, são divididos em tipos de acordo com sua finalidade funcional

  1. Nós de grupo (osg :: Group) - são a classe base para todos os nós intermediários e são projetados para combinar outros nós em grupos
  2. Nós de transformação (osg :: Transform e seus descendentes) - projetados para descrever a transformação das coordenadas do objeto
  3. Nós geométricos (osg :: Geode) - nós terminais (folha) do gráfico de cena que contêm informações sobre um ou mais objetos geométricos.

A geometria dos objetos de cena no OSG é descrita em seu próprio sistema de coordenadas local do objeto. Os nós de transformação localizados entre esse objeto e o nó raiz implementam transformações de coordenadas da matriz para obter a posição do objeto no sistema de coordenadas base.

Os nós executam muitas funções importantes, em particular, armazenam o estado da exibição de objetos, e esse estado afeta apenas o subgráfico associado a este nó. Vários retornos de chamada podem ser associados a nós no gráfico de cena, manipuladores de eventos que permitem alterar o estado do nó e o subgráfico associado a ele.

Todas as operações globais no gráfico de cena associadas à obtenção do resultado final na tela são realizadas automaticamente pelo mecanismo, percorrendo periodicamente o gráfico em profundidade.

No exemplo examinado pela última vez , nossa cena consistia em um único objeto - um modelo de avião carregado de um arquivo. Olhando muito adiante, direi que este modelo é o nó da folha do gráfico de cena. É fortemente soldado ao sistema de coordenadas de base global do motor.

2. gerenciamento de memória OSG


Como os nós do gráfico de cena armazenam muitos dados sobre objetos e operações de cena, é necessário alocar memória, inclusive dinamicamente, para armazenar esses dados. Nesse caso, ao manipular o gráfico de cena e, por exemplo, excluir alguns de seus nós, você precisa monitorar cuidadosamente que os nós excluídos do gráfico não são mais processados. Esse processo é sempre acompanhado por erros, depuração demorada, pois é muito difícil para o desenvolvedor rastrear quais ponteiros para objetos se referem aos dados existentes e quais devem ser excluídos. Sem um gerenciamento de memória eficaz, é mais provável que ocorram erros de segmentação e vazamentos de memória.

O gerenciamento de memória é uma tarefa crítica no OSG e seu conceito é baseado em dois pontos:

  1. Alocação de memória: garantindo a alocação da quantidade de memória necessária para armazenar um objeto.
  2. Liberar memória: retorne a memória alocada ao sistema quando não for necessária.

Muitas linguagens de programação modernas, como C #, Java, Visual Basic .Net e similares, usam o chamado coletor de lixo para liberar memória alocada. O conceito da linguagem C ++ não fornece essa abordagem, no entanto, podemos imitá-la usando os chamados ponteiros inteligentes.

Hoje, o C ++ possui ponteiros inteligentes em seu arsenal, que é chamado de pronto para uso (e o padrão C ++ 17 já conseguiu livrar a linguagem de alguns tipos obsoletos de ponteiros inteligentes), mas nem sempre foi esse o caso. A primeira das versões oficiais do OSG, com o número de 0,9, nasceu em 2002 e havia mais três anos antes do primeiro lançamento oficial. Naquela época, o padrão C ++ ainda não fornecia indicadores inteligentes e, mesmo que você acredite em uma digressão histórica , o próprio idioma passava por momentos difíceis. Portanto, a aparência de uma bicicleta na forma de seus próprios indicadores inteligentes, implementados no OSG, não é de todo surpreendente. Esse mecanismo está profundamente integrado à estrutura do motor, portanto é absolutamente necessário entender sua operação desde o início.

3. As classes osg :: ref_ptr <> e osg :: Referenced


O OSG fornece seu próprio mecanismo de ponteiro inteligente com base na classe de modelo osg :: ref_ptr <> para implementar a coleta de lixo automática. Para sua operação adequada, o OSG fornece outra classe osg :: Referenced para gerenciar blocos de memória para os quais a referência a eles é contada.

A classe osg :: ref_ptr <> fornece vários operadores e métodos.

  • get () é um método público que retorna um ponteiro bruto, por exemplo, ao usar o modelo osg :: Node como argumento, esse método retornará osg :: Node *.
  • operador * () é realmente o operador de desreferência.
  • operator -> () e operator = () - permitem que você use osg :: ref_ptr <> como um ponteiro clássico ao acessar os métodos e propriedades dos objetos descritos por este ponteiro.
  • operador == (), operador! = () e operador! () - permitem executar operações de comparação em ponteiros inteligentes.
  • valid () é um método público que retorna true se o ponteiro gerenciado tiver o valor correto (não NULL). A expressão some_ptr.valid () é equivalente à expressão some_ptr! = NULL se some_ptr for um ponteiro inteligente.
  • release () é um método público, útil quando você deseja retornar um endereço gerenciado de uma função. Sobre isso será descrito em mais detalhes posteriormente.

A classe osg :: Referenced é a classe base para todos os elementos do gráfico de cena, como nós, geometria, estados de renderização e outros objetos colocados no palco. Assim, ao criar o nó raiz da cena, herdamos indiretamente toda a funcionalidade fornecida pela classe osg :: Referenced. Portanto, em nosso programa, há um anúncio

osg::ref_ptr<osg::Node> root; 

A classe osg :: Referenced contém um contador inteiro para referências ao bloco de memória alocado. Este contador é inicializado como zero no construtor da classe. Ele é incrementado em um quando o objeto osg :: ref_ptr <> é criado. Esse contador diminui assim que qualquer referência ao objeto descrito por este ponteiro é excluída. Um objeto é destruído automaticamente quando qualquer ponteiro inteligente deixa de fazer referência a ele.

A classe osg :: Referenced possui três métodos públicos:

  • ref () é um método público que incrementa em 1 contagem de referência.
  • unref () é um método público, diminuindo em 1 contagem de referência.
  • referenceCount () é um método público que retorna o valor atual do contador de referência, que é útil ao depurar código.

Esses métodos estão disponíveis em todas as classes derivadas de osg :: Referenced. No entanto, deve-se lembrar que o controle manual do contador de links pode levar a consequências imprevisíveis e, usando isso, você deve entender claramente o que está fazendo.

4. Como o OSG coleta o lixo e por que é necessário


Há várias razões pelas quais ponteiros inteligentes e coleta de lixo devem ser usados:

  • Minimização de erros críticos: o uso de ponteiros inteligentes permite automatizar a alocação e liberação de memória. Não há indicadores brutos perigosos.
  • Gerenciamento eficaz de memória: a memória alocada para o objeto é liberada imediatamente, assim que o objeto se torna desnecessário, o que leva ao uso econômico dos recursos do sistema.
  • Facilitação da depuração de aplicativos: com a capacidade de rastrear claramente o número de links para um objeto, temos oportunidades para vários tipos de otimizações e experimentos.

Suponha que um gráfico de cena consista em um nó raiz e em vários níveis de nós filhos. Se o nó raiz e todos os nós filhos forem gerenciados usando a classe osg :: ref_ptr <>, o aplicativo poderá rastrear apenas o ponteiro para o nó raiz. A remoção desse nó resultará em uma remoção automática e seqüencial de todos os nós filhos.



Ponteiros inteligentes podem ser usados ​​como variáveis ​​locais, variáveis ​​globais, membros da classe e diminuir automaticamente a contagem de referência quando o ponteiro inteligente ficar fora do escopo.

Indicadores inteligentes são fortemente recomendados pelos desenvolvedores de OSG para uso em projetos, mas há alguns pontos fundamentais que você deve prestar atenção:

  • Instâncias de osg :: Referenced e seus derivados podem ser criados exclusivamente no heap. Eles não podem ser criados na pilha como variáveis ​​locais, pois os destruidores dessas classes são declarados como protegidos. Por exemplo

 osg::ref_ptr<osg::Node> node = new osg::Node; //  osg::Node node; //  

  • Você pode criar nós de cena temporários usando ponteiros C ++ regulares; no entanto, essa abordagem não é segura. É melhor usar ponteiros inteligentes para garantir que o gráfico da cena seja gerenciado corretamente.

 osg::Node *tmpNode = new osg::Node; //  ,  ... osg::ref_ptr<osg::Node> node = tmpNode; //         ! 

  • Em nenhum caso você deve usar cenas de link cíclico na árvore quando o nó se referir a si mesmo direta ou indiretamente através de vários níveis



No gráfico de exemplo do gráfico de cena, o nó Filho 1.1 se refere a si próprio e o nó Filho 2.2 também se refere ao nó Filho 1.2. Esse tipo de link pode levar ao cálculo incorreto do número de links e ao comportamento indefinido do programa.

5. Rastreando objetos gerenciados


Para ilustrar a operação do mecanismo de ponteiro inteligente no OSG, escrevemos o seguinte exemplo sintético

main.h

 #ifndef MAIN_H #define MAIN_H #include <osg/ref_ptr> #include <osg/Referenced> #include <iostream> #endif // MAIN_H 

main.cpp

 #include "main.h" class MonitoringTarget : public osg::Referenced { public: MonitoringTarget(int id) : _id(id) { std::cout << "Constructing target " << _id << std::endl; } protected: virtual ~MonitoringTarget() { std::cout << "Dsetroying target " << _id << std::endl; } int _id; }; int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(0); std::cout << "Referenced count before referring: " << target->referenceCount() << std::endl; osg::ref_ptr<MonitoringTarget> anotherTarget = target; std::cout << "Referenced count after referring: " << target->referenceCount() << std::endl; return 0; } 

Criamos uma classe descendente osg :: Referenced que não faz nada, exceto no construtor e destruidor que informa que sua instância foi criada e exibe o identificador que é determinado quando a instância é criada. Crie uma instância da classe usando o mecanismo de ponteiro inteligente

 osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(0); 

Em seguida, exibimos o contador de referência para o objeto de destino

 std::cout << "Referenced count before referring: " << target->referenceCount() << std::endl; 

Depois disso, crie um novo ponteiro inteligente, atribuindo o valor do ponteiro anterior

 osg::ref_ptr<MonitoringTarget> anotherTarget = target; 

e novamente exibir o contador de referência

 std::cout << "Referenced count after referring: " << target->referenceCount() << std::endl; 

Vamos ver o que obtivemos analisando a saída do programa

 15:42:39:   Constructing target 0 Referenced count before referring: 1 Referenced count after referring: 2 Dsetroying target 0 15:42:42:   

Quando o construtor da classe é iniciado, uma mensagem correspondente é exibida, informando que a memória do objeto está alocada e o construtor funcionou bem. Além disso, depois de criar um ponteiro inteligente, vemos que o contador de referência para o objeto criado aumentou um. Criando um novo ponteiro, atribuindo a ele o valor do ponteiro antigo - essencialmente criando um novo link para o mesmo objeto, para que o contador de referência seja incrementado por outro. Quando o programa termina, o destruidor da classe MonitoringTarget é chamado.



Vamos conduzir outro experimento adicionando esse código ao final da função main ()

 for (int i = 1; i < 5; i++) { osg::ref_ptr<MonitoringTarget> subTarget = new MonitoringTarget(i); } 

levando a um programa "exaustivo"

 16:04:30:   Constructing target 0 Referenced count before referring: 1 Referenced count after referring: 2 Constructing target 1 Dsetroying target 1 Constructing target 2 Dsetroying target 2 Constructing target 3 Dsetroying target 3 Constructing target 4 Dsetroying target 4 Dsetroying target 0 16:04:32:   

Criamos vários objetos no corpo do loop, usando um ponteiro inteligente. Como o escopo do ponteiro se estende, neste caso, apenas ao corpo do loop, quando ele sai, o destruidor é chamado automaticamente. Isso não aconteceria, obviamente, usaríamos os indicadores usuais.

Com a liberação automática de memória, é outra característica importante do trabalho com ponteiros inteligentes. Como o destruidor da classe derivada osg :: Referenced está protegido, não podemos chamar explicitamente o operador delete para excluir o objeto. A única maneira de excluir um objeto é redefinir o número de links para ele. Porém, nosso código se torna inseguro durante o processamento de dados multiencadeados - podemos acessar um objeto já excluído de outro encadeamento.

Felizmente, o OSG fornece uma solução para esse problema com a ajuda de seu agendador de remoção de objetos. Este planejador é baseado no uso da classe osg :: DeleteHandler. Funciona de tal maneira que não executa a operação de excluir um objeto imediatamente, mas executa-o após um tempo. Todos os objetos a serem excluídos são armazenados temporariamente até o momento da exclusão segura e, em seguida, todos são excluídos de uma só vez. O agendador de remoção osg :: DeleteHandler é controlado pelo back-end de renderização OSG.

6. Retorno da função


Adicione a seguinte função ao nosso código de exemplo

 MonitoringTarget *createMonitoringTarget(int id) { osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(id); return target.release(); } 

e substitua a chamada para o novo operador no loop pela chamada para esta função

 for (int i = 1; i < 5; i++) { osg::ref_ptr<MonitoringTarget> subTarget = createMonitoringTarget(i); } 

A chamada release () reduzirá o número de referências ao objeto para zero, mas, em vez de excluir a memória, ela retornará o ponteiro real à memória alocada diretamente. Se esse ponteiro for atribuído a outro ponteiro inteligente, não haverá vazamento de memória.

Conclusões


Os conceitos do gráfico de cena e ponteiros inteligentes são básicos para entender o princípio de operação e, portanto, o uso efetivo do OpenSceneGraph. Com relação aos ponteiros inteligentes OSG, lembre-se de que seu uso é absolutamente essencial quando

  • Espera-se o armazenamento a longo prazo da instalação.
  • Um objeto armazena um link para outro objeto
  • Você deve retornar um ponteiro de uma função

O código de exemplo fornecido no artigo está disponível aqui .

Para continuar ...

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


All Articles