
Introduccion
En alguna parte de las lecciones anteriores, ya se dijo que OSG admite la carga de diversos tipos de recursos, como imágenes ráster, modelos 3D de varios formatos o, por ejemplo, fuentes a través de su propio sistema de complemento. El complemento OSG es un componente separado que extiende la funcionalidad del motor y tiene una interfaz estandarizada dentro del OSG. El complemento se implementa como una biblioteca dinámica compartida (dll en Windows, Linux, etc.). Los nombres de las bibliotecas de complementos corresponden a una convención específica
osgdb_< >.dll
es decir, el nombre del complemento siempre contiene el prefijo osgdb_. Una extensión de archivo le dice al motor qué complemento se debe usar para descargar un archivo con esta extensión. Por ejemplo, cuando escribimos una función en código
osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("cessna.osg");
el motor ve la extensión osg y carga un complemento llamado osgdb_osg.dll (u osgdb_osg.so en el caso de Linux). El código del complemento hace todo el trabajo sucio devolviéndonos un puntero a un nodo que describe el modelo cessna. Del mismo modo, tratando de cargar una imagen PNG
osg::ref_ptr<osg:Image> image = osgDB::readImageFile("picture.png");
hará que se cargue el complemento osgdb_png.dll, que implementa un algoritmo para leer datos de una imagen PNG y colocar estos datos en un objeto de tipo osg :: Image.
Todas las operaciones para trabajar con recursos externos se implementan mediante las funciones de la biblioteca osgDB, con la que invariablemente vinculamos los programas de un ejemplo a otro. Esta biblioteca se basa en el sistema de complemento OSG. Hasta la fecha, el paquete OSG incluye muchos complementos que funcionan con la mayoría de los formatos de imagen, modelos 3D y fuentes utilizados en la práctica. Los complementos proporcionan datos de lectura (importación) de un formato específico y, en la mayoría de los casos, escritura de datos en un archivo del formato requerido (exportación). La utilidad osgconv, en particular, le permite convertir datos de un formato a otro, por ejemplo, el sistema de complemento.
$ osgconv cessna.osg cessna.3ds
convierte fácil y naturalmente el modelo cessna osg en formato 3DS, que luego puede importarse a un editor 3D, por ejemplo, a Blender (por cierto, hay una
extensión para trabajar con osg directamente para Blender)

Hay una lista oficial de complementos OSG estándar con una descripción de su propósito, pero es larga y soy demasiado vaga para traerla aquí. Es más fácil mirar la ruta de instalación de la biblioteca en la carpeta bin / ospPlugins-xyz, donde x, y, z es el número de versión de OSG. Desde el nombre del archivo de complemento, es fácil entender qué formato procesa.
Si el compilador MinGW compila el OSG, se agrega un prefijo adicional mingw_ al nombre estándar del complemento, es decir, el nombre se verá así
mingw_osgdb_< >.dll
La versión del complemento compilado en la configuración DEBUG también está equipada con el sufijo d al final del nombre, es decir, el formato será
osgdb_< >d.dll
o
mingw_osgdb_< >d.dll
cuando se ensambla MinGW.
1. Complementos pseudo-cargadores
Algunos complementos OSG realizan las funciones de los llamados pseudocargadores: esto significa que no están vinculados a una extensión de archivo específica, pero al agregar un sufijo al final del nombre del archivo, puede especificar qué complemento se debe usar para descargar este archivo, por ejemplo
$ osgviewer worldmap.shp.ogr
En este caso, el nombre real del archivo en el disco es worldmap.shp: este archivo almacena un mapa mundial en formato de archivo ESRI. El sufijo .ogr le dice a la biblioteca osgDB que use el complemento osgdb_ogr para cargar este archivo; de lo contrario, se usará el complemento osgdb_shp.
Otro buen ejemplo es el complemento osgdb_ffmpeg. La biblioteca FFmpeg admite más de 100 códecs diferentes. Para leer cualquiera de ellos, simplemente podemos agregar el sufijo .ffmpeg después del nombre del archivo multimedia.
Además de esto, algunos pseudocargadores nos permiten pasar a través de un sufijo una serie de parámetros que afectan el estado del objeto cargado, y ya lo encontramos en un ejemplo con animación
node = osgDB::readNodeFile("cessna.osg.0,0,90.rot");
La línea 0.90 indica al complemento osgdb_osg los parámetros de la orientación inicial del modelo cargado. Algunos pseudo-cargadores requieren parámetros completamente específicos para funcionar.
2. API para desarrollar complementos de terceros
Es completamente lógico si, después de toda la lectura, tuvo la idea de que probablemente no sería difícil escribir su propio complemento para OSG, lo que le permitiría importar un formato no estándar de imágenes o modelos 3D. Y este es un pensamiento verdadero! El mecanismo del complemento está diseñado para expandir la funcionalidad del motor sin cambiar el OSG. Para comprender los principios básicos de escribir un complemento, intentemos implementar un ejemplo simple.
El desarrollo del complemento consiste en expandir la interfaz virtual de lectura / escritura proporcionada por OSG. Esta funcionalidad es proporcionada por la clase virtual osgDB :: ReaderWriter. Esta clase proporciona una serie de métodos virtuales redefinidos por el desarrollador del complemento.
Método | Descripción |
---|
supportsExtensions () | Acepta dos parámetros de cadena: extensión de archivo y descripción. El método siempre se llama en el constructor de la subclase. |
acceptExtension () | Devuelve verdadero si la extensión pasada como argumento es compatible con el complemento |
fileExists () | Le permite determinar si existe un archivo determinado (la ruta se pasa como un parámetro) en el disco (devuelve verdadero si tiene éxito) |
readNode () | Acepta el nombre del archivo y las opciones como un objeto osgDB :: Option. El desarrollador implementa las funciones para leer datos de un archivo |
writeNode () | Acepta el nombre del nodo, el nombre del archivo deseado y las opciones. El desarrollador implementa las funciones de escritura de datos en el disco |
readImage () | Lectura de datos de mapa de bits del disco |
writeImage () | Escribir un mapa de bits en el disco |
La implementación del método readNode () se puede describir mediante el siguiente código
osgDB::ReaderWriter::ReadResult readNode( const std::string &file, const osgDB::Options *options) const {
Es un poco sorprendente que, en lugar de un puntero al nodo del gráfico de escena, el método devuelva el tipo osgDB :: ReaderWriter :: ReadResult. Este tipo es un objeto de resultado de lectura y se puede usar como contenedor de nodo, imagen, enumerador de estado (por ejemplo, FILE_NOT_FOUND), otro objeto especial o incluso como una cadena de mensaje de error. Tiene muchos constructores implícitos para implementar las funciones descritas.
Otra clase útil es osgDB :: Options. Puede permitirle establecer u obtener una serie de opciones de carga utilizando los métodos setOptionString () y getOptionString (). También se permite pasar esta cadena al constructor de esta clase como argumento.
El desarrollador puede controlar el comportamiento del complemento configurando la configuración en la cadena de parámetros que se pasa al cargar el objeto, por ejemplo, de esta manera
3. Procesamiento del flujo de datos en el complemento OSG
La clase base osgDB :: ReaderWriter incluye un conjunto de métodos que procesan los datos de los flujos de entrada / salida proporcionados por la biblioteca estándar de C ++. La única diferencia entre estos métodos de lectura / escritura y los discutidos anteriormente es que, en lugar del nombre del archivo, aceptan std :: istream & input streams o std :: ostream & output stream. Usar un flujo de E / S de archivo siempre es preferible a usar un nombre de archivo. Para realizar operaciones de lectura de archivos, podemos usar el siguiente diseño de interfaz:
osgDB::ReaderWriter::ReadResult readNode( const std::string &file, const osgDB::Options *options) const { ... osgDB::ifstream stream(file.c_str(), std::ios::binary); if (!stream) return ReadResult::ERROR_IN_READING_FILE; return readNode(stream, options); } ... osgDB::ReaderWriter::ReadResult readNode( std::istream &stream, const osgDB::Options *options) const {
Después de implementar el complemento, podemos usar las funciones estándar osgDB :: readNodeFile () y osgDB :: readImageFile () para cargar modelos e imágenes, simplemente especificando la ruta del archivo. OSG encontrará y descargará el complemento que escribimos.
4. Escribimos nuestro propio complemento
Por lo tanto, nadie nos molesta en crear nuestro propio formato para almacenar datos en geometría tridimensional, y lo inventaremos.
piramide.pmd vertex: 1.0 1.0 0.0 vertex: 1.0 -1.0 0.0 vertex: -1.0 -1.0 0.0 vertex: -1.0 1.0 0.0 vertex: 0.0 0.0 2.0 face: 0 1 2 3 face: 0 3 4 face: 1 0 4 face: 2 1 4 face: 3 2 4
Aquí al principio del archivo hay una lista de vértices con sus coordenadas. Los índices de vértices van en orden, comenzando desde cero. Después de la lista de vértices viene una lista de caras. Cada cara está definida por una lista de índices de vértices a partir de los cuales se forma. Al parecer, nada complicado. La tarea es leer este archivo desde el disco y formar una geometría tridimensional sobre su base.
5. Configuración del proyecto del complemento: características del script de compilación
Si antes creábamos aplicaciones, ahora tenemos que escribir una biblioteca dinámica, y no solo una biblioteca, sino un complemento OSG que satisfaga ciertos requisitos. Comenzaremos a cumplir estos requisitos con un script de compilación del proyecto que se verá así
plugin.pro TEMPLATE = lib CONFIG += plugin CONFIG += no_plugin_name_prefix TARGET = osgdb_pmd win32-g++: TARGET = $$join(TARGET,,mingw_,) win32 { OSG_LIB_DIRECTORY = $$(OSG_BIN_PATH) OSG_INCLUDE_DIRECTORY = $$(OSG_INCLUDE_PATH) DESTDIR = $$(OSG_PLUGINS_PATH) CONFIG(debug, debug|release) { TARGET = $$join(TARGET,,,d) LIBS += -L$$OSG_LIB_DIRECTORY -losgd LIBS += -L$$OSG_LIB_DIRECTORY -losgViewerd LIBS += -L$$OSG_LIB_DIRECTORY -losgDBd LIBS += -L$$OSG_LIB_DIRECTORY -lOpenThreadsd LIBS += -L$$OSG_LIB_DIRECTORY -losgUtild } else { LIBS += -L$$OSG_LIB_DIRECTORY -losg LIBS += -L$$OSG_LIB_DIRECTORY -losgViewer LIBS += -L$$OSG_LIB_DIRECTORY -losgDB LIBS += -L$$OSG_LIB_DIRECTORY -lOpenThreads LIBS += -L$$OSG_LIB_DIRECTORY -losgUtil } INCLUDEPATH += $$OSG_INCLUDE_DIRECTORY } unix { DESTDIR = /usr/lib/osgPlugins-3.7.0 CONFIG(debug, debug|release) { TARGET = $$join(TARGET,,,d) LIBS += -losgd LIBS += -losgViewerd LIBS += -losgDBd LIBS += -lOpenThreadsd LIBS += -losgUtild } else { LIBS += -losg LIBS += -losgViewer LIBS += -losgDB LIBS += -lOpenThreads LIBS += -losgUtil } } INCLUDEPATH += ./include HEADERS += $$files(./include/*.h) SOURCES += $$files(./src/*.cpp)
Analizaremos los matices individuales con más detalle.
TEMPLATE = lib
significa que construiremos la biblioteca. Para evitar la generación de enlaces simbólicos con la ayuda de qué problemas de conflictos de versiones de la biblioteca se resuelven en los sistemas * nix, le indicamos al sistema de compilación que esta biblioteca será un complemento, es decir, se cargará en la memoria "sobre la marcha"
CONFIG += plugin
A continuación, excluimos la generación del prefijo lib, que se agrega al usar los compiladores de la familia gcc y que se tiene en cuenta en el entorno de tiempo de ejecución al cargar la biblioteca
CONFIG += no_plugin_name_prefix
Establecer el nombre del archivo de biblioteca
TARGET = osgdb_pmd
donde pmd es la extensión de archivo del formato del modelo 3D que inventamos. Además, debemos indicar que en el caso del ensamblaje MinGW, el prefijo mingw_ se agrega necesariamente al nombre
win32-g++: TARGET = $$join(TARGET,,mingw_,)
Especifique la ruta de compilación de la biblioteca: para Windows
DESTDIR = $$(OSG_PLUGINS_PATH)
para linux
DESTDIR = /usr/lib/osgPlugins-3.7.0
Para Linux, con tal indicación de la ruta (que sin duda es una muleta, pero aún no he encontrado otra solución), le damos el derecho de escribir en la carpeta especificada con complementos OSG de un usuario ordinario
# chmod 666 /usr/lib/osgPlugins-3.7.0
Todas las demás configuraciones de compilación son similares a las utilizadas en el ensamblaje de aplicaciones de muestra anteriores.
6. Configuración del proyecto del complemento: características del modo de depuración
Dado que este proyecto es una biblioteca dinámica, debe haber un programa que cargue esta biblioteca en el proceso de su ejecución. Puede ser cualquier aplicación que use OSG y en la que se llamará a la función
node = osdDB::readNodeFile("piramide.pmd");
En este caso, nuestro complemento se cargará. Para no escribir un programa de este tipo, usaremos una solución preparada: el visor estándar de osgviewer, que se incluye en el paquete de entrega del motor. Si en la consola ejecuta
$ osgviewer piramide.pmd
entonces también activará el complemento. En la configuración de inicio del proyecto, especifique la ruta a osgviewerd, como el directorio de trabajo, especifique el directorio donde se encuentra el archivo piramide.pmd y especifique el mismo archivo en las opciones de la línea de comandos de osgviewer

Ahora podemos ejecutar el complemento y depurarlo directamente desde QtCreator IDE.
6. Implementamos el marco del complemento
Este ejemplo, en cierta medida, generaliza el conocimiento que ya hemos recibido sobre OSG de lecciones anteriores. Al escribir un complemento, tenemos que
- Seleccione una estructura de datos para almacenar la información de geometría del modelo leída de un archivo de modelo
- Leer y analizar (analizar) el archivo de datos del modelo
- Configure correctamente el objeto geométrico osg :: Drawable basado en los datos leídos del archivo
- Construye un subgrafo de escena para un modelo cargado
Entonces, por tradición, daré el código fuente completo del complemento
Complemento Osgdb_pmdmain.h #ifndef MAIN_H #define MAIN_H #include <osg/Geometry> #include <osg/Geode> #include <osgDB/FileNameUtils> #include <osgDB/FileUtils> #include <osgDB/Registry> #include <osgUtil/SmoothingVisitor> //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ struct face_t { std::vector<unsigned int> indices; }; //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ struct pmd_mesh_t { osg::ref_ptr<osg::Vec3Array> vertices; osg::ref_ptr<osg::Vec3Array> normals; std::vector<face_t> faces; pmd_mesh_t() : vertices(new osg::Vec3Array) , normals(new osg::Vec3Array) { } osg::Vec3 calcFaceNormal(const face_t &face) const { osg::Vec3 v0 = (*vertices)[face.indices[0]]; osg::Vec3 v1 = (*vertices)[face.indices[1]]; osg::Vec3 v2 = (*vertices)[face.indices[2]]; osg::Vec3 n = (v1 - v0) ^ (v2 - v0); return n * (1 / n.length()); } }; //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class ReaderWriterPMD : public osgDB::ReaderWriter { public: ReaderWriterPMD(); virtual ReadResult readNode(const std::string &filename, const osgDB::Options *options) const; virtual ReadResult readNode(std::istream &stream, const osgDB::Options *options) const; private: pmd_mesh_t parsePMD(std::istream &stream) const; std::vector<std::string> parseLine(const std::string &line) const; }; #endif
main.cpp #include "main.h"
Primero, cuidemos las estructuras para almacenar datos de geometría.
struct face_t { std::vector<unsigned int> indices; };
- describe la cara definida por la lista de índices de los vértices que pertenecen a esta cara. El modelo como un todo será descrito por dicha estructura.
struct pmd_mesh_t { osg::ref_ptr<osg::Vec3Array> vertices; osg::ref_ptr<osg::Vec3Array> normals; std::vector<face_t> faces; pmd_mesh_t() : vertices(new osg::Vec3Array) , normals(new osg::Vec3Array) { } osg::Vec3 calcFaceNormal(const face_t &face) const { osg::Vec3 v0 = (*vertices)[face.indices[0]]; osg::Vec3 v1 = (*vertices)[face.indices[1]]; osg::Vec3 v2 = (*vertices)[face.indices[2]]; osg::Vec3 n = (v1 - v0) ^ (v2 - v0); return n * (1 / n.length()); } };
La estructura consta de variables miembro para almacenar datos: vértices: para almacenar una matriz de vértices de un objeto geométrico; normales: una serie de normales a las caras del objeto; caras: una lista de caras del objeto. El constructor de estructura inicializa inmediatamente punteros inteligentes
pmd_mesh_t() : vertices(new osg::Vec3Array) , normals(new osg::Vec3Array) { }
Además, la estructura contiene un método que le permite calcular el vector normal de la cara calcFaceNormal () como un parámetro que toma una estructura que describe la cara. Todavía no entraremos en detalles sobre la implementación de este método, los analizaremos un poco más tarde.
Por lo tanto, decidimos sobre las estructuras en las que almacenaremos los datos de geometría. Ahora escribamos el marco de nuestro complemento, es decir, implementamos la clase de herencia osgDB :: ReaderWriter
class ReaderWriterPMD : public osgDB::ReaderWriter { public: ReaderWriterPMD(); virtual ReadResult readNode(const std::string &filename, const osgDB::Options *options) const; virtual ReadResult readNode(std::istream &stream, const osgDB::Options *options) const; private: pmd_mesh_t parsePMD(std::istream &stream) const; std::vector<std::string> parseLine(const std::string &line) const; };
Como se recomienda en la descripción de la API para desarrollar complementos, en esta clase redefinimos los métodos para leer datos de un archivo y convertirlos en un subgrafo de la escena. El método readNode () realiza dos sobrecargas: una acepta el nombre del archivo como entrada y la otra recibe una secuencia de entrada estándar. El constructor de la clase define las extensiones de archivo compatibles con el complemento.
ReaderWriterPMD::ReaderWriterPMD() { supportsExtension("pmd", "PMD model file"); }
La primera sobrecarga del método readNode () analiza la exactitud del nombre y la ruta del archivo, asocia una secuencia de entrada estándar con el archivo y llama a la segunda sobrecarga, que hace el trabajo principal
osgDB::ReaderWriter::ReadResult ReaderWriterPMD::readNode( const std::string &filename, const osgDB::Options *options) const {
En la segunda sobrecarga, implementamos el algoritmo de generación de objetos para OSG
osgDB::ReaderWriter::ReadResult ReaderWriterPMD::readNode( std::istream &stream, const osgDB::Options *options) const { (void) options;
Al final del archivo main.cpp, llame a la macro REGISTER_OSGPLUGIN ().
REGISTER_OSGPLUGIN( pmd, ReaderWriterPMD )
Esta macro genera código adicional que permite a OSG, en forma de biblioteca osgDB, construir un objeto de tipo ReaderWriterPMD y llamar a sus métodos para cargar archivos de tipo pmd. Por lo tanto, el marco del complemento está listo, la cosa queda por pequeña: implementar la carga y el análisis del archivo pmd.
7. archivo de modelo 3D Parsim
Ahora toda la funcionalidad del complemento se basa en la implementación del método parsePMD ()
pmd_mesh_t ReaderWriterPMD::parsePMD(std::istream &stream) const { pmd_mesh_t mesh;
El método ParseLine () analiza la línea del archivo pmd std::vector<std::string> ReaderWriterPMD::parseLine(const std::string &line) const { std::vector<std::string> tokens;
Este método convertirá la cadena "vértice: 1.0 -1.0 0.0" en una lista de las dos líneas "vértice" y "1.0 -1.0 0.0". En la primera línea identificamos el tipo de datos: el vértice o la cara, a partir de la segunda extraemos los datos en las coordenadas del vértice. Para garantizar el funcionamiento de este método, necesitamos la función auxiliar delete_symbol (), que elimina el carácter dado de la cadena y devuelve una cadena que no contiene este carácter std::string delete_symbol(const std::string &str, char symbol) { std::string tmp = str; tmp.erase(std::remove(tmp.begin(), tmp.end(), symbol), tmp.end()); return tmp; }
Es decir, ahora hemos implementado toda la funcionalidad de nuestro complemento y podemos probarlo.8. Probar el complemento
Compilamos el complemento y ejecutamos la depuración (F5). Se lanzará una versión de depuración del visor estándar osgviewerd, que analizará el archivo piramide.pmd que se le pasó, cargará nuestro complemento y llamará a su método readNode (). Si hicimos todo bien, obtendremos ese resultado.
Resulta que la lista de vértices y caras en nuestro archivo inventado del modelo 3D ocultaba una pirámide cuadrangular.¿Por qué calculamos las normales nosotros mismos? En una de las lecciones, se nos ofreció el siguiente método de cálculo automático de normales suavizadas osgUtil::SmoothingVisitor::smooth(*geom);
Aplicamos esta función en nuestro ejemplo, en lugar de asignar nuestras propias normales
y obtenemos el siguiente resultado: las
normales afectan el cálculo de la iluminación del modelo, y vemos que en esta situación las normales suavizadas conducen a resultados incorrectos del cálculo de la iluminación de la pirámide. Es por esta razón que aplicamos nuestra bicicleta al cálculo de las normales. Pero creo que explicar los matices de esto está más allá del alcance de esta lección.