OpenSceneGraph: Técnicas básicas de programación

imagen

Introduccion


Este artículo se centrará no tanto en los gráficos como en cómo debe organizarse la aplicación que lo usa, teniendo en cuenta las características específicas del motor OpenSceneGraph y el software que proporciona.

No es ningún secreto que la clave del éxito de cualquier producto de software es una arquitectura bien diseñada que brinde la capacidad de mantener y expandir el código escrito. En este sentido, el motor que estamos considerando está en un nivel bastante alto, proporcionando al desarrollador un conjunto de herramientas muy amplio, que proporciona la construcción de una arquitectura modular flexible.

Este artículo es bastante extenso e incluye una descripción general de las diversas herramientas y técnicas (patrones de diseño, si lo desea) proporcionadas por el motor del desarrollador. Todas las secciones del artículo se proporcionan con ejemplos, cuyo código se puede tomar en mi repositorio .

1. Opciones de línea de comando de análisis


En C / C ++, los parámetros de la línea de comandos se pasan a través de los argumentos a la función main (). En ejemplos anteriores, marcamos cuidadosamente estos parámetros como no utilizados, ahora los usaremos para decirle a nuestro programa algunos datos cuando comience.

OSG tiene herramientas de análisis de línea de comandos incorporadas.

Crea el siguiente ejemplo

Ejemplo de línea 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(); } 


Establecer los parámetros de inicio del programa en QtCreator



Al ejecutar el programa para su ejecución, obtenemos el resultado (modelo de camión tomado del mismo OpenSceneGraph-Data )



Ahora veamos un ejemplo línea por línea

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

crea una instancia de la clase de analizador de línea de comando osg :: ArgumentParser. Cuando se crea, el constructor de clase pasa los argumentos aceptados por la función main () del sistema operativo.

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

analizamos los argumentos, es decir, buscamos la clave "–modelo" entre ellos, colocando su valor en el nombre del archivo de cadena. Por lo tanto, utilizando esta clave, transferimos el nombre del archivo con un modelo tridimensional al programa. Luego cargamos este modelo y lo mostramos

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

El método read () de la clase osg :: ArgumentParser tiene muchas sobrecargas, lo que le permite leer no solo valores de cadena desde la línea de comando, sino también enteros, números de punto flotante, vectores, etc. Por ejemplo, puede leer un determinado parámetro de tipo float

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

Si este parámetro no se muestra en la línea de comando, su valor permanecerá como estaba después de que se inicializó la variable de tamaño.

2. Mecanismo de notificación y registro.


OpenSceneGraph tiene un mecanismo de notificación que le permite mostrar mensajes de depuración durante el proceso de representación, así como también iniciado por el desarrollador. Esta es una gran ayuda al rastrear y depurar un programa. El sistema de notificación OSG admite la salida de información de diagnóstico (errores, advertencias, notificaciones) en el nivel central del motor y sus complementos. El desarrollador puede mostrar un mensaje de diagnóstico durante la operación del programa utilizando la función osg :: notify ().

Esta función funciona como un flujo de salida estándar de la biblioteca C ++ estándar a través de la sobrecarga del operador <<. Como argumento, toma el nivel de mensaje: SIEMPRE, FATAL, WARN, NOTICE, INFO, DEBUG_INFO y DEBUG_FP. Por ejemplo

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

muestra una advertencia con texto definido por el usuario.

Las notificaciones OSG pueden contener información importante sobre el estado del programa, extensiones del subsistema de gráficos de la computadora, posibles problemas con el motor.

En algunos casos, es necesario enviar estos datos no a la consola, sino poder redirigir esta salida a un archivo (en forma de registro) o a cualquier otra interfaz, incluido un widget gráfico. El motor contiene una clase especial osg :: NotifyHandler que proporciona la redirección de notificaciones a la secuencia de salida que necesita el desarrollador.

Usando un ejemplo simple, considere cómo puede redirigir la salida de notificaciones, por ejemplo, a un archivo de registro de texto. Escribe el siguiente código

Notificar ejemplo
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 redirigir la salida, escribimos la clase LogFileHandler, que es la sucesora de osg :: NotifyHandler. El constructor y el destructor de esta clase controlan la apertura y el cierre de la secuencia de salida _log con la que está asociado el archivo de texto. El método notify () es un método de clase base similar que redefinimos para enviar a las notificaciones de archivo enviadas por OSG durante la operación a través del parámetro msg.

Class 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; }; 

Luego, en el programa principal, realice las configuraciones necesarias

 osg::setNotifyLevel(osg::INFO); 

establezca el nivel de notificaciones INFO, es decir, la salida al registro de toda la información sobre el funcionamiento del motor, incluidas las notificaciones actuales de funcionamiento normal.

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

Instale el controlador de notificaciones. A continuación, procesamos argumentos de línea de comando en los que se pasan las rutas a los modelos cargados

 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; } 

Al mismo tiempo, manejamos la situación de falta de datos en la línea de comando, mostrando un mensaje en el modo manual de registro utilizando la macro OSG_FATAL. Ejecute el programa con los siguientes argumentos



obtener salida a un archivo de registro como este

Ejemplo de registro 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 


No importa que en este momento esta información le parezca inútil, en el futuro, tal conclusión puede ayudar a depurar errores en su programa.

De manera predeterminada, OSG envía mensajes a la salida estándar std :: cout y mensajes de error a la secuencia std :: cerr. Sin embargo, al anular el controlador de notificaciones, como se muestra en el ejemplo, esta salida se puede redirigir a cualquier flujo de salida, incluidos los elementos de la GUI.

Tenga en cuenta que al establecer un alto nivel de notificaciones (por ejemplo, FATAL), el sistema ignora todas las notificaciones de un nivel inferior. Por ejemplo, en un caso similar

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

simplemente no se mostrará un mensaje personalizado.

3. Intercepción de atributos geométricos.


La clase osg :: Geometry gestiona un conjunto de datos que describen vértices y muestra una malla poligonal utilizando un conjunto ordenado de primitivas. Sin embargo, esta clase no tiene idea sobre elementos de la topología del modelo como caras, aristas y la relación entre ellos. Este matiz impide la implementación de cosas como mover ciertas caras, por ejemplo, al animar modelos. OSG no admite actualmente esta funcionalidad.

Sin embargo, el motor implementa una serie de functores que le permiten volver a leer los atributos de geometría de cualquier objeto y usarlos para modelar la topología de la malla poligonal. En C ++, un functor es una construcción que le permite usar un objeto como función.

La clase osg :: Drawable proporciona al desarrollador cuatro tipos de functores:

  1. osg :: Drawable :: AttributeFunctor - lee los atributos de los vértices como una matriz de punteros. Tiene varios métodos virtuales para aplicar atributos de vértice de diferentes tipos de datos. Para usar este functor, debe describir la clase y anular uno o más de sus métodos, dentro de los cuales se realizan las acciones requeridas por el desarrollador


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

  1. osg :: Drawable :: ConstAttributeFunctor - versión de solo lectura del functor anterior: se pasa un puntero a una matriz de vectores como parámetro constante
  2. osg :: PrimitiveFunctor - imita el proceso de renderizar objetos OpenGL. Con el pretexto de representar un objeto, se llaman los métodos de función anulados por el desarrollador. Este functor tiene dos subclases de plantilla importantes: osg :: TemplatePrimitiveFunctor <> y osg :: TriangleFunctor <>. Estas clases reciben vértices primitivos como parámetros y los pasan a métodos de usuario utilizando el operador operator ().
  3. osg :: PrimitiveIndexFunctor: realiza las mismas acciones que el functor anterior, pero acepta los índices de vértice de la primitiva como parámetro.

Las clases derivadas de osg :: Drawable, como osg :: ShapeDrawable y osg :: Geometry, tienen un método accept () que le permite aplicar varios functores.

4. Ejemplo de uso del functor primitivo


Ilustramos la funcionalidad descrita utilizando el ejemplo de recopilación de información sobre caras triangulares y puntos de cierta geometría que determinamos previamente.

Ejemplo 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(); } 


Omitiendo el proceso de creación de geometría considerado por nosotros muchas veces, prestemos atención a lo siguiente. Definimos una estructura FaceCollector para la cual redefinimos operator () de la siguiente manera

 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, cuando se lo llama, mostrará las coordenadas de los tres vértices que le transmite el motor. La función vec2str es necesaria para traducir los componentes del vector osg :: Vec3 a std :: string. Para llamar al functor, cree una instancia de él y páselo al objeto de geometría a través del método accept ()

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

Esta llamada, como se mencionó anteriormente, imita la representación de la geometría, reemplazando el dibujo en sí mismo al llamar a un método de función anulada. En este caso, se llamará durante el "dibujo" de cada uno de los triángulos que componen la geometría del ejemplo.

En la pantalla obtenemos tal geometría



y tal escape a la consola

 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 hecho, cuando se llama a geom-> accept (...), los triángulos no se procesan, las llamadas de OpenGL se simulan y, en lugar de ellas, los datos sobre los vértices del triángulo, cuya representación se simula



La clase osg :: TemplatePrimitiveFunctor recopila datos no solo sobre triángulos, sino también sobre cualquier otra primitiva OpenGL. Para implementar el procesamiento de estos datos, debe anular los siguientes operadores en el argumento de plantilla

 //   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. El patrón de visitante


El patrón de visitante se utiliza para acceder a operaciones para cambiar los elementos del gráfico de escena sin cambiar las clases de estos elementos. La clase visitante implementa todas las funciones virtuales relevantes para aplicarlas a varios tipos de elementos a través de un mecanismo de despacho dual. Con este mecanismo, el desarrollador puede crear su propia instancia de visitante mediante la implementación de la funcionalidad que necesita con la ayuda de operadores especiales y vincular al visitante a varios tipos de elementos de gráficos de escena sobre la marcha, sin cambiar la funcionalidad de los elementos mismos. Esta es una excelente manera de extender la funcionalidad de un elemento sin definir subclases de estos elementos.

Para implementar este mecanismo en OSG, se define la clase osg :: NodeVisitor. La clase heredada de osg :: NodeVisitor se mueve alrededor del gráfico de escena, visita cada nodo y le aplica las operaciones definidas por el desarrollador. Esta es la clase principal utilizada para intervenir en el proceso de actualización de nodos y recorte de nodos invisibles, así como también para aplicar algunas otras operaciones relacionadas con la modificación de la geometría de los nodos en la escena, como osgUtil :: SmoothingVisitor, osgUtil :: Simplifier y osgUtil :: TriStripVisitor.

Para subclasificar al visitante, debemos anular uno o más métodos de aplicación () sobrecargados virtuales proporcionados por la clase base osg :: NodeVisitor. La mayoría de los principales tipos de nodos OSG tienen estos métodos. El visitante llamará automáticamente al método apply () para cada uno de los nodos visitados al recorrer el gráfico de la escena. El desarrollador anula el método apply () para cada uno de los tipos de nodo que necesita.

En la implementación del método apply (), el desarrollador, en el momento apropiado, debe llamar al método traverse () de la clase base osg :: NodeVisitor. Esto inicia la transición del visitante al siguiente nodo, ya sea un hijo o un vecino en el nivel de jerarquía, si el nodo actual no tiene ningún nodo hijo al que se pueda hacer la transición. La ausencia de una llamada a traverse () significa detener el recorrido del gráfico de escena y se ignora el resto del gráfico de escena.

Las sobrecargas del método apply () tienen formatos unificados

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

Para omitir el subgrafo del nodo actual para el objeto visitante, debe establecer el modo de rastreo, por ejemplo,

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

El modo de derivación lo establecen varios enumeradores

  1. TRAVERSE_ALL_CHILDREN: se mueve por todos los nodos secundarios.
  2. TRAVERSE_PARENTS: pasa del nodo actual sin llegar al nodo raíz
  3. TRAVERSE_ACTIVE_CHILDREN: omite los nodos exclusivamente activos, es decir, aquellos cuya visibilidad se activa a través del nodo osg :: Switch.


6. Análisis de la estructura del cessna ardiente.


El desarrollador siempre puede analizar esa parte del gráfico de escena que genera el modelo cargado desde el archivo.

Ejemplo 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(); } 


Creamos la clase InfoVisitor, heredando 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; }; 

La propiedad _level protegida apuntará al nivel del gráfico de escena en el que se encuentra actualmente nuestra clase de visitante. En el constructor, inicialice el contador de nivel y establezca el modo de recorrido de nodo para evitar todos los nodos secundarios.

Ahora redefina los métodos apply () para nodos

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

Aquí mostraremos el tipo del nodo actual. El método libraryName () para el nodo muestra el nombre de la biblioteca OSG donde se implementa este nodo, y el método className muestra el nombre de la clase de nodo. Estos métodos se implementan mediante el uso de macros en el código de las bibliotecas OSG.

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

Después de eso, aumentamos el contador de nivel de gráfico y llamamos al método traverse (), iniciando una transición a un nivel superior, al nodo hijo. Después de regresar de traverse (), nuevamente disminuimos el valor del contador. Es fácil adivinar que traverse () inicia una llamada repetida al método apply (), traverse repetido () ya para un subgráfico que comienza desde el nodo actual. Obtenemos la ejecución recursiva de visitantes hasta llegar a los nodos finales del gráfico de escena.

Para un nodo final de tipo osg :: Geode, se anula su sobrecarga del método apply ()

 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--; } 

con código de trabajo similar, excepto que mostramos datos en todos los objetos geométricos adjuntos al nodo geométrico actual

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

En la función main (), procesamos argumentos de línea de comando a través de los cuales pasamos una lista de modelos cargados en la escena y formamos la escena

 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; } 

Al mismo tiempo, procesamos errores relacionados con la ausencia de nombres de archivo de modelo en la línea de comandos. Ahora creamos una clase de visitante y la pasamos al gráfico de escena para ejecutar

 InfoVisitor infoVisitor; root->accept(infoVisitor); 

Los siguientes son los pasos para iniciar el visor, que ya hemos hecho muchas veces. Después de comenzar el programa con parámetros

 $ visitor ../data/cessnafire.osg 

veremos la siguiente salida a la consola

 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 hecho, tenemos un árbol completo de la escena cargada. Disculpe, ¿dónde hay tantos nodos? Todo es muy simple: los modelos del formato * .osg en sí mismos son contenedores que almacenan no solo la geometría del modelo, sino también otra información sobre su estructura en forma de un subgráfico de la escena OSG. La geometría del modelo, las transformaciones, los efectos de partículas que producen humo y llamas son todos nodos del gráfico de escena OSG.Cualquier escena puede descargarse desde * .osg o descargarse desde el visor al formato * .osg.

Este es un ejemplo simple de aplicar la mecánica del visitante. De hecho, dentro de los visitantes, puede realizar muchas operaciones para modificar los nodos durante la ejecución del programa.

7. Controlar el comportamiento de los nodos en el gráfico de escena anulando el método traverse ()


Una forma importante de trabajar con OSG es anular el método traverse (). Este método se llama cada vez que se dibuja un marco. Aceptan un parámetro de tipo osg :: NodeVisitor y que informa qué paso del gráfico de escena se está realizando actualmente (actualización, procesamiento de eventos o recorte). La mayoría de los hosts OSG anulan este método para implementar su funcionalidad.

Debe recordarse que anular el método traverse () puede ser peligroso, ya que afecta el proceso de atravesar el gráfico de la escena y puede conducir a una visualización incorrecta de la escena. También es inconveniente si desea agregar una nueva funcionalidad a varios tipos de nodos. En este caso, se utilizan devoluciones de llamada de nodo, cuya conversación se reducirá un poco.

Ya sabemos que el nodo osg :: Switch puede controlar la visualización de sus nodos secundarios, incluida la visualización de algunos nodos y desactivar la visualización de otros. Pero él no sabe cómo hacer esto automáticamente, por lo que crearemos un nuevo nodo basado en el antiguo, que cambiará entre nodos secundarios en diferentes puntos en el tiempo, de acuerdo con el valor del contador interno.

Ejemplo 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(); } 


Echemos un vistazo a este ejemplo. Estamos creando una nueva clase AnimatingSwitch que hereda 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 clase contiene el constructor predeterminado.

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

y constructor para copiar, creado de acuerdo con los requisitos de OSG

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

El constructor para copiar debe contener como parámetros: una referencia constante a la instancia de clase que se copiará y el parámetro osg :: CopyOp que especifica la configuración de copia de la clase. Siguen letras bastante extrañas

 META_Node(osg, AnimatingSwitch); 

Esta es una macro que forma la estructura necesaria para el descendiente de una clase derivada de osg :: Node. Hasta que le demos importancia a esta macro, es importante que esté presente al heredar de osg :: Switch al definir todas las clases descendientes. La clase contiene el campo protegido _count, el contador en función del cual cambiamos. Implementamos el cambio al anular el método traverse ()

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

Cambiar el estado de visualización de los nodos ocurrirá cada vez que el valor del contador (incrementando cada llamada al método) es un múltiplo de 60. Compilamos el ejemplo y lo ejecutamos



Dado que el método traverse () se redefine constantemente para varios tipos de nodos, debería proporcionar un mecanismo para obtener matrices de transformación y estados de representación para su uso posterior por su algoritmo sobrecargado. El parámetro de entrada osg :: NodeVisitor es la clave para varias operaciones con nodos. En particular, indica el tipo de recorrido actual del gráfico de escena, como la actualización, el procesamiento de eventos y el recorte de caras invisibles. Los dos primeros están relacionados con devoluciones de llamada de nodos y se tendrán en cuenta al estudiar la animación.

El paso de recorte se puede identificar convirtiendo el objeto osg :: NodeVisitor en osg :: CullVisitor

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


8. Mecanismo de devolución de llamada


En el artículo anterior, implementamos la animación de un objeto de escena cambiando los parámetros de su transformación dentro del ciclo de representación de la escena. Como se ha mencionado muchas veces, este enfoque contiene un comportamiento de aplicación potencialmente peligroso en el procesamiento de subprocesos múltiples. Para resolver este problema, se utiliza un mecanismo de devolución de llamada que se realiza al recorrer el gráfico de escena.

Hay varios tipos de devoluciones de llamada en el motor. Las devoluciones de llamada se implementan mediante clases especiales, entre las cuales osg :: NodeCallback está diseñado para manejar el proceso de actualización de nodos de escena, y osg :: Drawable :: UpdateCallback, osg :: Drawable :: EventCallback y osg :: Drawable: CullCallback - realiza las mismas funciones, pero para objetos de geometría.

La clase osg :: NodeCallback tiene un operador de operador virtual () reemplazable proporcionado por el desarrollador para implementar su propia funcionalidad. Para que la devolución de llamada funcione, debe adjuntar una instancia de la clase de llamada al nodo para el que se procesará llamando al método setUpdateCallback () o addUpdateCallback (). El operador operator () se llama automáticamente durante la actualización de los nodos en el gráfico de escena cuando se procesa cada fotograma.

La siguiente tabla enumera las devoluciones de llamada disponibles para el desarrollador en OSG.

NombreFunctor de devolución de llamadaMétodo virtualMétodo para adjuntar a un objeto
Actualización de nodoosg :: NodeCallbackoperador ()osg :: Node :: setUpdateCallback ()
Evento de nodoosg :: NodeCallbackoperador ()osg :: Node :: setEventCallback ()
Recorte de nodososg :: NodeCallbackoperador ()osg :: Node :: setCullCallback ()
Actualización de geometríaosg::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


Un poco más arriba, escribimos un ejemplo con el cambio de dos modelos de aviones. Ahora repetiremos este ejemplo, pero haremos todo correctamente utilizando el mecanismo de devolución de llamada OSG.

Callbacks con ejemplo
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(); } 


Debe crear una clase heredada de osg :: NodeCallback, que controla el nodo osg :: Switch

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

El contador _count controlará el cambio del nodo osg :: Switch de mapear un nodo hijo a otro, dependiendo de su valor. En el constructor, inicializamos el contador y redefinimos el método del operador virtual ()

 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); } 

El nodo en el que trabajó la llamada se le pasa a través del parámetro de nodo. Como sabemos con certeza que este será un nodo del tipo osg :: Switch, realizamos una conversión estática del puntero al nodo al puntero al nodo del interruptor

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

Cambiaremos los nodos secundarios mostrados con el valor válido de este puntero, y cuando el valor del contador sea un múltiplo de 60

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

No olvides llamar al método traverse () para continuar el recorrido recursivo del gráfico de escena

 traverse(node, nv); 

El resto del código del programa es trivial, excepto la línea.

 root->setUpdateCallback( new SwitchingCallback ); 

donde asignamos la devolución de llamada que creamos al nodo raíz de tipo osg :: Switch. El programa funciona de manera similar al ejemplo anterior.



Hasta ahora, hemos utilizado el misterioso método traverse () para dos propósitos: anular este método en las clases sucesoras y llamar a este método en la clase osg :: NodeVisitor para continuar atravesando el gráfico de la escena.

En el ejemplo que acabamos de examinar, utilizamos la tercera opción de llamar a traverse (), pasando un puntero al nodo y un puntero a la instancia del visitante como parámetros. Como en los dos primeros casos, si no hay una llamada a traverse () en este nodo, se detendrá el rastreo del gráfico de escena.

Los métodos addUpdateCallback () también sirven para agregar una devolución de llamada al nodo. A diferencia de setUpdateCallback (), se usa para agregar otra devolución de llamada a las existentes. Por lo tanto, puede haber múltiples devoluciones de llamada para el mismo nodo.

Conclusión


Examinamos las técnicas básicas utilizadas en el desarrollo de aplicaciones utilizando el motor gráfico OpenSceneGraph. Sin embargo, esto está lejos de todos los puntos que me gustaría mencionar (a pesar de que el artículo resultó ser bastante largo), por lo tanto

Continuará ...

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


All Articles