OpenSceneGraph: Gráfico de escena y punteros inteligentes

imagen

Introduccion


En un artículo anterior, miramos el ensamblado OpenSceneGraph desde la fuente y escribimos un ejemplo elemental en el que un avión gris cuelga en un mundo púrpura vacío. Estoy de acuerdo, no demasiado impresionante. Sin embargo, como dije antes, en este pequeño ejemplo, existen los conceptos principales en los que se basa este motor gráfico. Consideremos con más detalle. El material a continuación utiliza ilustraciones del blog de Alexander Bobkov sobre OSG (es una pena que el autor haya abandonado la escritura sobre OSG ...). El artículo también se basa en material y ejemplos del libro OpenSceneGraph 3.0. Guía para principiantes

Debo decir que la publicación anterior fue objeto de algunas críticas, con lo cual estoy parcialmente de acuerdo: el material salió sin decir y fuera de contexto. Intentaré arreglar esta omisión debajo del corte.

1. Brevemente sobre el gráfico de la escena y sus nodos.


El concepto central del motor es el llamado gráfico de escena (no es coincidencia que se haya quedado atascado en el nombre del marco mismo), una estructura jerárquica de árbol que le permite organizar una representación lógica y espacial de una escena tridimensional. El gráfico de escena contiene el nodo raíz y sus nodos o nodos intermedios y terminales asociados.

Por ejemplo



Este gráfico representa una escena que consiste en una casa y una mesa en ella. La casa tiene una cierta representación geométrica y está ubicada de cierta manera en el espacio en relación con algún sistema de coordenadas básico asociado con el nodo raíz (raíz). La tabla también se describe mediante cierta geometría, ubicada de alguna manera en relación con la casa, y junto con la casa, en relación con el nodo raíz. Todos los nodos, que tienen una propiedad común, porque heredan de una clase osg :: Node, se dividen en tipos de acuerdo con su propósito funcional

  1. Nodos de grupo (osg :: Group): son la clase base para todos los nodos intermedios y están diseñados para combinar otros nodos en grupos
  2. Nodos de transformación (osg :: Transform y sus descendientes): diseñados para describir la transformación de coordenadas de objeto
  3. Nodos geométricos (osg :: Geode): nodos terminales (hoja) del gráfico de escena que contienen información sobre uno o más objetos geométricos.

La geometría de los objetos de escena en OSG se describe en su propio sistema de coordenadas local del objeto. Los nodos de transformación ubicados entre este objeto y el nodo raíz implementan transformaciones de coordenadas de matriz para obtener la posición del objeto en el sistema de coordenadas base.

Los nodos realizan muchas funciones importantes, en particular, almacenan el estado de la visualización de objetos, y este estado afecta solo el subgrafo asociado con este nodo. Se pueden asociar varias devoluciones de llamada con nodos en el gráfico de escena, controladores de eventos que le permiten cambiar el estado del nodo y el subgrafo asociado con él.

El motor realiza automáticamente todas las operaciones globales en el gráfico de escena asociadas con la obtención del resultado final en la pantalla, recorriendo periódicamente el gráfico en profundidad.

En el ejemplo examinado la última vez , nuestra escena consistía en un solo objeto: un modelo de avión cargado desde un archivo. Mirando muy lejos, diré que este modelo es el nodo hoja del gráfico de escena. Está firmemente soldado al sistema de coordenadas de base global del motor.

2. gestión de la memoria OSG


Dado que los nodos del gráfico de escena almacenan una gran cantidad de datos sobre objetos de escena y operaciones en ellos, es necesario asignar memoria, incluso dinámicamente, para almacenar estos datos. En este caso, al manipular el gráfico de escena y, por ejemplo, al eliminar algunos de sus nodos, debe controlar cuidadosamente que los nodos eliminados del gráfico ya no se procesen. Este proceso siempre va acompañado de errores, depuración que lleva mucho tiempo, ya que es bastante difícil para el desarrollador rastrear qué punteros a objetos se refieren a datos existentes y cuáles deben eliminarse. Sin una administración de memoria efectiva, es más probable que ocurran errores de segmentación y pérdidas de memoria.

La gestión de la memoria es una tarea crítica en OSG y su concepto se basa en dos puntos:

  1. Asignación de memoria: asegura la asignación de la cantidad de memoria necesaria para almacenar un objeto.
  2. Liberar memoria: devuelve la memoria asignada al sistema cuando no es necesario.

Muchos lenguajes de programación modernos, como C #, Java, Visual Basic .Net y similares, utilizan el llamado recolector de basura para liberar memoria asignada. El concepto del lenguaje C ++ no proporciona tal enfoque, pero podemos imitarlo usando los llamados punteros inteligentes.

Hoy en día, C ++ tiene punteros inteligentes en su arsenal, que se llama "listo para usar" (y el estándar C ++ 17 ya ha logrado eliminar el lenguaje de algunos tipos obsoletos de punteros inteligentes), pero este no siempre ha sido el caso. La primera de las versiones oficiales de OSG numeradas 0.9 nació en 2002, y hubo tres años más antes del primer lanzamiento oficial. En ese momento, el estándar C ++ aún no proporcionaba punteros inteligentes, e incluso si cree en una digresión histórica , el lenguaje en sí estaba pasando por tiempos difíciles. Por lo tanto, la apariencia de una bicicleta en forma de sus propios punteros inteligentes, que se implementan en OSG, no es nada sorprendente. Este mecanismo está profundamente integrado en la estructura del motor, por lo que comprender su funcionamiento es absolutamente necesario desde el principio.

3. Las clases referenciadas osg :: ref_ptr <> y osg ::


OSG proporciona su propio mecanismo de puntero inteligente basado en la clase de plantilla osg :: ref_ptr <> para implementar la recolección automática de basura. Para su correcto funcionamiento, OSG proporciona otra clase osg :: Referenced para administrar bloques de memoria para los cuales se cuenta la referencia a ellos.

La clase osg :: ref_ptr <> proporciona varios operadores y métodos.

  • get () es un método público que devuelve un puntero sin formato, por ejemplo, cuando se usa la plantilla osg :: Node como argumento, este método devolverá osg :: Node *.
  • operator * () es en realidad el operador de desreferencia.
  • operator -> () y operator = () - le permite usar osg :: ref_ptr <> como un puntero clásico al acceder a los métodos y propiedades de los objetos descritos por este puntero.
  • operator == (), operator! = () y operator! (): le permiten realizar operaciones de comparación en punteros inteligentes.
  • valid () es un método público que devuelve true si el puntero administrado tiene el valor correcto (no NULL). La expresión some_ptr.valid () es equivalente a la expresión some_ptr! = NULL si some_ptr es un puntero inteligente.
  • release () es un método público, útil cuando desea devolver una dirección administrada de una función. Sobre esto se describirá con más detalle más adelante.

La clase osg :: Referenced es la clase base para todos los elementos del gráfico de escena, como nodos, geometría, estados de representación y otros objetos colocados en el escenario. Por lo tanto, al crear el nodo raíz de la escena, heredamos indirectamente toda la funcionalidad proporcionada por la clase osg :: Referenced. Por lo tanto, en nuestro programa hay un anuncio

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

La clase osg :: Referenced contiene un contador entero para referencias al bloque de memoria asignado. Este contador se inicializa a cero en el constructor de la clase. Se incrementa en uno cuando se crea el objeto osg :: ref_ptr <>. Este contador disminuye en cuanto se elimina cualquier referencia al objeto descrito por este puntero. Un objeto se destruye automáticamente cuando los punteros inteligentes dejan de hacer referencia a él.

La clase osg :: Referenced tiene tres métodos públicos:

  • ref () es un método público que se incrementa en 1 recuento de referencia.
  • unref () es un método público, que disminuye en 1 recuento de referencia.
  • referenceCount () es un método público que devuelve el valor actual del contador de referencia, que es útil al depurar código.

Estos métodos están disponibles en todas las clases derivadas de osg :: Referenced. Sin embargo, debe recordarse que el control manual del contador de enlaces puede tener consecuencias impredecibles, y al usar esto debe comprender claramente lo que está haciendo.

4. Cómo OSG recolecta basura y por qué es necesaria


Hay varias razones por las cuales se deben utilizar punteros inteligentes y recolección de basura:

  • Minimización de errores críticos: el uso de punteros inteligentes le permite automatizar la asignación y la liberación de memoria. No hay punteros crudos peligrosos.
  • Gestión eficaz de la memoria: la memoria asignada para el objeto se libera inmediatamente, tan pronto como el objeto se vuelve innecesario, lo que conduce al uso económico de los recursos del sistema.
  • Facilitación de la depuración de aplicaciones: al tener la capacidad de rastrear claramente el número de enlaces a un objeto, tenemos oportunidades para varios tipos de optimizaciones y experimentos.

Suponga que un gráfico de escena consta de un nodo raíz y varios niveles de nodos secundarios. Si el nodo raíz y todos los nodos secundarios se administran utilizando la clase osg :: ref_ptr <>, la aplicación solo puede rastrear el puntero al nodo raíz. Eliminar este nodo dará como resultado una eliminación secuencial y automática de todos los nodos secundarios.



Los punteros inteligentes se pueden usar como variables locales, variables globales, miembros de la clase y disminuyen automáticamente el recuento de referencia cuando el puntero inteligente se sale del alcance.

Los desarrolladores de OSG recomiendan encarecidamente los punteros inteligentes para su uso en proyectos, pero hay algunos puntos fundamentales a los que debe prestar atención:

  • Las instancias de osg :: Referenced y sus derivados se pueden crear exclusivamente en el montón. No se pueden crear en la pila como variables locales, ya que los destructores de estas clases se declaran protegidos. Por ejemplo

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

  • Puede crear nodos de escena temporales utilizando punteros regulares de C ++, sin embargo, este enfoque será inseguro. Es mejor utilizar punteros inteligentes para garantizar que el gráfico de la escena se gestione correctamente.

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

  • En ningún caso debe usar escenas de enlace cíclico en el árbol cuando el nodo se refiere a sí mismo directa o indirectamente a través de varios niveles



En el gráfico de ejemplo del gráfico de escena, el nodo Child 1.1 se refiere a sí mismo, y el nodo Child 2.2 también se refiere al nodo Child 1.2. Este tipo de enlaces puede conducir a un cálculo incorrecto del número de enlaces y al comportamiento indefinido del programa.

5. Seguimiento de objetos gestionados


Para ilustrar el funcionamiento del mecanismo de puntero inteligente en OSG, escribimos el siguiente ejemplo 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; } 

Creamos una clase descendiente osg :: referenciada que no hace nada excepto en el constructor y destructor que informa que su instancia se creó y muestra el identificador que se determina cuando se crea la instancia. Cree una instancia de la clase utilizando el mecanismo de puntero inteligente

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

A continuación, mostramos el contador de referencia para el objeto de destino.

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

Después de eso, cree un nuevo puntero inteligente, asignándole el valor del puntero anterior

 osg::ref_ptr<MonitoringTarget> anotherTarget = target; 

y nuevamente muestra el contador de referencia

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

Veamos qué obtuvimos analizando la salida del programa.

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

Cuando se inicia el constructor de la clase, se muestra un mensaje correspondiente que nos dice que la memoria para el objeto está asignada y que el constructor funcionó bien. Además, después de crear un puntero inteligente, vemos que el contador de referencia para el objeto creado ha aumentado en uno. Crear un nuevo puntero, asignarle el valor del viejo puntero es esencialmente crear un nuevo enlace al mismo objeto, por lo que el contador de referencia se incrementa en otro. Cuando el programa sale, se llama al destructor de la clase MonitoringTarget.



Realicemos otro experimento agregando dicho código al final de la función main ()

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

conduciendo a tal programa de "escape"

 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:   

Creamos varios objetos en el cuerpo del bucle, utilizando un puntero inteligente. Dado que el alcance del puntero se extiende en este caso solo al cuerpo del bucle, cuando sale, se llama automáticamente al destructor. Esto no sucedería, obviamente, usaríamos los punteros habituales.

Con la liberación automática de memoria es otra característica importante de trabajar con punteros inteligentes. Dado que el destructor de clase derivada osg :: Referenced está protegido, no podemos llamar explícitamente al operador delete para eliminar el objeto. La única forma de eliminar un objeto es restablecer el número de enlaces a él. Pero luego nuestro código se vuelve inseguro durante el procesamiento de datos de subprocesos múltiples: podemos acceder a un objeto ya eliminado desde otro hilo.

Afortunadamente, OSG proporciona una solución a este problema con la ayuda de su programador de eliminación de objetos. Este planificador se basa en el uso de la clase osg :: DeleteHandler. Funciona de tal manera que no realiza la operación de eliminar un objeto inmediatamente, sino que lo realiza después de un tiempo. Todos los objetos que se eliminarán se almacenan temporalmente hasta que llegue el momento de una eliminación segura, y luego se eliminan todos a la vez. El programador de eliminación osg :: DeleteHandler es controlado por el backend de renderizado OSG.

6. Regreso de la función


Agregue la siguiente función a nuestro código de ejemplo

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

y reemplace la llamada al nuevo operador en el bucle con la llamada a esta función

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

La llamada release () reducirá el número de referencias al objeto a cero, pero en lugar de eliminar la memoria, devuelve el puntero real a la memoria asignada directamente. Si este puntero se asigna a otro puntero inteligente, no habrá pérdidas de memoria.

Conclusiones


Los conceptos del gráfico de escena y los punteros inteligentes son básicos para comprender el principio de funcionamiento y, por lo tanto, el uso efectivo de OpenSceneGraph. En cuanto a los punteros inteligentes OSG, recuerde que su uso es absolutamente esencial cuando

  • Se espera el almacenamiento a largo plazo de la instalación.
  • Un objeto almacena un enlace a otro objeto
  • Debe devolver un puntero de una función.

El código de muestra proporcionado en el artículo está disponible aquí .

Continuará ...

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


All Articles