
引言
我们已经研究了
一个示例 ,其中他们用彩虹的所有颜色绘制了一个正方形。 尽管如此,还有另一种技术,即应用于所谓的
纹理贴图或仅纹理的三维几何图形-二维光栅图像。 在这种情况下,效果不会影响几何图形的顶点,而是会更改场景栅格化过程中获得的所有像素的数据。 该技术可以显着提高最终图像的真实感和细节。
OSG支持多种纹理属性和纹理化模式。 但是,在讨论纹理之前,让我们先讨论一下OSG如何处理位图图像。 为了处理光栅图像,提供了一个特殊的类-osg :: Image,该类将图像数据存储在其中,最终目的是使对象纹理化。
1.栅格图像数据的表示。 OSG类::图片
从磁盘加载映像的最佳方法是使用osgDB :: readImageFile()调用。 它与osg :: readNodeFile()调用非常相似,后者已经使我们感到无聊。 如果我们有一个名为picture.bmp的位图,则将其加载如下所示
osg::ref_ptr<osg::Image> image = osgDB::readImageFile("picture.bmp");
如果正确加载了图像,则指针将是有效的,否则该函数将返回NULL。 下载后,我们可以使用以下公共方法获取图像信息
- t(),s()和r()-返回图像的宽度,高度和深度。
- data()-返回“原始”图像数据类型为unsigned char *的指针。 通过该指针,开发人员可以直接对图像数据进行操作。 您可以使用getPixalFormat()和getDataType()方法来了解图像数据格式。 它们返回的值等效于OpenGL函数glTexImage *()的格式和类型的参数。 例如,如果图片具有GL_RGB像素格式,并且类型为GL_UNSIGNED_BYTE,则使用三个独立的元素(无符号字节)来表示RGB颜色分量

您可以创建一个新的图像对象并为其分配内存。
osg::ref_ptr<osg::Image> image = new osg::Image; image->allocateImage(s, t, r, GL_RGB, GL_UNSIGNED_BYTE); unsigned char *ptr = image->data();
s,t,r是图像大小; GL_RGB设置像素格式,而GL_UNSIGNED_BYTE设置数据类型以描述单个颜色分量。 所需大小的内部数据缓冲区分配在内存中,如果没有单个链接指向该映像,则会自动销毁该缓冲区。
OSG插件系统支持下载几乎所有流行的图像格式:* .jpg,* .bmp,* .png,* .tif等。 通过编写自己的插件可以轻松扩展此列表,但这是另一个讨论的主题。
2.纹理化的基础
要将纹理应用于三维模型,您需要执行许多步骤:
- 定义几何对象的顶点的纹理坐标(在3D设计者的环境中,这称为UV扫描)。
- 为1D,2D,3D或立方纹理创建纹理属性对象。
- 为纹理属性设置一个或多个图像。
- 将纹理属性和模式附加到应用于绘制对象的状态集。
OSG定义了osg :: Texture类,该类封装了各种纹理。 子类osg :: Texture1D,osg :: Texture2D,osg :: Texture3D和osg :: TextureCubeMap继承自它们,它们表示OpenGL中采用的各种纹理化技术。
最常见的osg :: Texture类方法是setImage(),它设置纹理中使用的图像,例如
osg::ref_ptr<osg::Image> image = osgDB::readImageFile("picture.bmp"); osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D; texture->setImage(image.get());
或者,您可以将图像对象直接传递到纹理类的构造函数
osg::ref_ptr<osg::Image> image = osgDB::readImageFile("picture.bmp"); osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D(image.get());
可以通过调用getImage()方法从纹理对象中检索图像。
另一个要点是为osg :: Geometry对象中的每个顶点设置纹理坐标。 这些坐标的传递通过调用setTexCoordArray()方法通过osg :: Vec2Array和osg :: Vec3Array数组进行。
设置纹理坐标后,我们需要设置纹理插槽编号(单位),因为OSG支持在同一几何图形上叠加多个纹理。 使用单个纹理时,单位编号始终为0。例如,以下代码说明了设置几何单位0的纹理坐标
osf::ref_ptr<osg::Vec2Array> texcoord = new osg::Vec2Array; texcoord->push_back( osg::Vec2(...) ); ... geom->setTexCoordArray(0, texcoord.get());
之后,我们可以将纹理属性添加到状态集,自动打开相应的纹理化模式(在我们的示例中为GL_TEXTURE_2D),然后将该属性应用于包含该几何的几何或节点
geom->getOrCreateStateSet()->setTextureAttributeAndModes(texture.get());
请注意,OpenGL管理视频卡图形内存中的图像数据,但是osg :: Image对象以及相同的数据位于系统内存中。 结果,我们将遇到这样的事实,即我们已经存储了相同数据的两个副本,占据了进程内存。 如果该图像没有被多个纹理属性共享,则在OpenGL将其传输到视频适配器内存后,可以立即将其从系统内存中删除。 osg :: Texture类提供了启用此功能的适当方法。
texture->setUnRefImageDataAfterApply( true );
3.加载并应用2D纹理
最常用的技术是2D纹理化-将二维图像叠加在三维表面的边缘上。 考虑将单个纹理应用于四边形多边形的最简单示例
纹理示例主文件 #ifndef MAIN_H #define MAIN_H #include <osg/Texture2D> #include <osg/Geometry> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif
main.cpp #include "main.h" 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.5f, 0.0f, -0.5f) ); vertices->push_back( osg::Vec3( 0.5f, 0.0f, -0.5f) ); vertices->push_back( osg::Vec3( 0.5f, 0.0f, 0.5f) ); vertices->push_back( osg::Vec3(-0.5f, 0.0f, 0.5f) ); osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array; normals->push_back( osg::Vec3(0.0f, -1.0f, 0.0f) ); osg::ref_ptr<osg::Vec2Array> texcoords = new osg::Vec2Array; texcoords->push_back( osg::Vec2(0.0f, 0.0f) ); texcoords->push_back( osg::Vec2(0.0f, 1.0f) ); texcoords->push_back( osg::Vec2(1.0f, 1.0f) ); texcoords->push_back( osg::Vec2(1.0f, 0.0f) ); osg::ref_ptr<osg::Geometry> quad = new osg::Geometry; quad->setVertexArray(vertices.get()); quad->setNormalArray(normals.get()); quad->setNormalBinding(osg::Geometry::BIND_OVERALL); quad->setTexCoordArray(0, texcoords.get()); quad->addPrimitiveSet( new osg::DrawArrays(GL_QUADS, 0, 4) ); osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D; osg::ref_ptr<osg::Image> image = osgDB::readImageFile("../data/Images/lz.rgb"); texture->setImage(image.get()); osg::ref_ptr<osg::Geode> root = new osg::Geode; root->addDrawable(quad.get()); root->getOrCreateStateSet()->setTextureAttributeAndModes(0, texture.get()); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); }
创建面的顶点和法线数组
osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array; vertices->push_back( osg::Vec3(-0.5f, 0.0f, -0.5f) ); vertices->push_back( osg::Vec3( 0.5f, 0.0f, -0.5f) ); vertices->push_back( osg::Vec3( 0.5f, 0.0f, 0.5f) ); vertices->push_back( osg::Vec3(-0.5f, 0.0f, 0.5f) ); osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array; normals->push_back( osg::Vec3(0.0f, -1.0f, 0.0f) );
创建纹理坐标数组
osg::ref_ptr<osg::Vec2Array> texcoords = new osg::Vec2Array; texcoords->push_back( osg::Vec2(0.0f, 0.0f) ); texcoords->push_back( osg::Vec2(0.0f, 1.0f) ); texcoords->push_back( osg::Vec2(1.0f, 1.0f) ); texcoords->push_back( osg::Vec2(1.0f, 0.0f) );
关键是,三维模型的每个顶点都对应于二维纹理上的一个点,并且纹理上这些点的坐标是相对的-将它们标准化为图像的实际宽度和高度。 我们要将整个加载的图像分别拉伸到一个正方形上,该正方形的角将对应于纹理点(0,0),(0,1),(1、1)和(1,0)。 顶点数组中顶点的顺序必须与纹理顶点的顺序匹配。
接下来,创建一个正方形,为几何分配一个顶点数组和一个法线数组
osg::ref_ptr<osg::Geometry> quad = new osg::Geometry; quad->setVertexArray(vertices.get()); quad->setNormalArray(normals.get()); quad->setNormalBinding(osg::Geometry::BIND_OVERALL); quad->setTexCoordArray(0, texcoords.get()); quad->addPrimitiveSet( new osg::DrawArrays(GL_QUADS, 0, 4) );
创建一个纹理对象并加载用于它的图像。
osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D; osg::ref_ptr<osg::Image> image = osgDB::readImageFile("../data/Images/lz.rgb"); texture->setImage(image.get());
创建场景的根节点,然后将我们创建的几何体放在此处
osg::ref_ptr<osg::Geode> root = new osg::Geode; root->addDrawable(quad.get());
最后将纹理属性应用于放置几何的节点
root->getOrCreateStateSet()->setTextureAttributeAndModes(0, texture.get());

osg :: Texture2D类确定纹理图像的大小是否为2的幂(例如64x64或256x512)的倍数,实际上是使用OpenGL gluScaleImage()函数自动缩放不适合大小的图像。 有一个setResizeNonPowerOfTwoHint()方法,用于确定是否调整图像的大小。 某些视频卡需要两倍于2的幂的图像大小,而osg :: Texture2D类支持使用任意纹理大小。
关于纹理融合的一些知识
正如我们已经说过的,纹理坐标从0标准化为1。点(0,0)对应于图像的左上角,点(1,1)对应于右下角。 如果将纹理坐标设置为大于1会怎样?
默认情况下,在OpenGL中,就像在OSG中一样,纹理将在轴的方向上重复,纹理坐标的值将超过1。 例如,经常使用此技术来创建长砖墙的模型,我使用小的纹理,在宽度和高度上重复其叠加多次。
可以通过osg :: Texture类的setWrap()方法控制此行为。 作为第一个参数,该方法采用应将混合模式应用到的轴的标识符作为第二个参数,例如
如果纹理坐标值超过1,则此代码明确告诉引擎沿s和r轴重复纹理。纹理映射模式的完整列表:
- 重复-重复纹理。
- 镜像-重复纹理,将其镜像。
- CLAMP_TO_EDGE-超出0到1的坐标将捕捉到纹理的相应边缘。
- CLAMP_TO_BORDER-坐标超出0到1时,将给出用户设置的边框颜色。
4.渲染到纹理
纹理渲染技术允许开发人员基于某些三维子场景或模型创建纹理并将其应用于主场景中的表面。 相似的技术通常称为纹理“烘焙”。
要动态烘焙纹理,必须完成三个步骤:
- 创建一个纹理对象以渲染到其中。
- 将场景渲染为纹理。
- 按预期使用生成的纹理。
我们必须创建一个空的纹理对象。 OSG允许您创建给定大小的空纹理。 setTextureSize()方法允许您设置纹理的宽度和高度,以及深度作为附加参数(对于3D纹理)。
若要渲染纹理,应通过调用attach()方法将其附加到相机对象,该方法将纹理对象作为参数。 另外,此方法接受一个参数,该参数指示应将帧缓冲区的哪一部分渲染到此纹理中。 例如,要将颜色缓冲区传输到纹理,应运行以下代码
camera->attach( osg::Camera::COLOR_BUFFER, texture.get() );
可用于渲染的帧缓冲区的其他部分包括深度缓冲区DEPTH_BUFFER,模板缓冲区STENCIL_BUFFER以及从COLOR_BUFFER0到COLOR_BUFFER15的其他颜色缓冲区。 附加颜色缓冲区的存在及其数量取决于视频卡的型号。
此外,对于渲染为纹理的相机,必须设置投影矩阵和视口的参数,其大小与纹理的大小相对应。 绘制每个帧时,纹理将更新。 请记住,不应将主摄像机用于渲染为纹理,因为它可以渲染主场景,而您只会看到黑屏。 仅当您执行屏幕外渲染时,可能无法满足此要求。
5.渲染到纹理的实现示例
为了演示纹理中的渲染技术,我们执行以下任务:创建一个正方形,在其上绘制一个正方形纹理,然后将动画场景渲染到该纹理中,当然要使用我们喜欢的cessna。 实现该示例的程序非常庞大。 但是,无论如何,我都会给出其完整的源代码。
Texrender示例主文件 #ifndef MAIN_H #define MAIN_H #include <osg/Camera> #include <osg/Texture2D> #include <osg/MatrixTransform> #include <osgDB/ReadFile> #include <osgGA/TrackballManipulator> #include <osgViewer/Viewer> #endif
main.cpp #include "main.h"
要创建一个正方形,编写一个单独的自由函数
osg::Geometry *createQuad(const osg::Vec3 &pos, float w, float h) { osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array; vertices->push_back( pos + osg::Vec3( w / 2, 0.0f, -h / 2) ); vertices->push_back( pos + osg::Vec3( w / 2, 0.0f, h / 2) ); vertices->push_back( pos + osg::Vec3(-w / 2, 0.0f, h / 2) ); vertices->push_back( pos + osg::Vec3(-w / 2, 0.0f, -h / 2) ); osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array; normals->push_back(osg::Vec3(0.0f, -1.0f, 0.0f)); osg::ref_ptr<osg::Vec2Array> texcoords = new osg::Vec2Array; texcoords->push_back( osg::Vec2(1.0f, 1.0f) ); texcoords->push_back( osg::Vec2(1.0f, 0.0f) ); texcoords->push_back( osg::Vec2(0.0f, 0.0f) ); texcoords->push_back( osg::Vec2(0.0f, 1.0f) ); osg::ref_ptr<osg::Geometry> quad = new osg::Geometry; quad->setVertexArray(vertices.get()); quad->setNormalArray(normals.get()); quad->setNormalBinding(osg::Geometry::BIND_OVERALL); quad->setTexCoordArray(0, texcoords.get()); quad->addPrimitiveSet(new osg::DrawArrays(GL_QUADS, 0, 4)); return quad.release(); }
该函数接受正方形中心的位置及其几何尺寸作为输入。 接下来,创建一个顶点数组,一个法线和纹理坐标数组,然后从该函数返回创建的几何图形。
在主程序的主体中,加载cessna模型
osg::ref_ptr<osg::Node> sub_model = osgDB::readNodeFile("../data/cessna.osg");
为了激活该模型,请创建并初始化围绕Z轴的旋转变换
osg::ref_ptr<osg::MatrixTransform> transform1 = new osg::MatrixTransform; transform1->setMatrix(osg::Matrix::rotate(0.0, osg::Vec3(0.0f, 0.0f, 1.0f))); transform1->addChild(sub_model.get());
现在为主要场景创建一个模型-我们将在其上渲染的正方形
osg::ref_ptr<osg::Geode> model = new osg::Geode; model->addChild(createQuad(osg::Vec3(0.0f, 0.0f, 0.0f), 2.0f, 2.0f));
使用RGBA像素格式(带有alpha通道的32位三分量颜色)为1024x1024像素正方形创建空纹理
int tex_widht = 1024; int tex_height = 1024; osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D; texture->setTextureSize(tex_widht, tex_height); texture->setInternalFormat(GL_RGBA); texture->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture2D::LINEAR); texture->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture2D::LINEAR);
将此纹理应用于正方形模型。
model->getOrCreateStateSet()->setTextureAttributeAndModes(0, texture.get());
然后创建一个可以烘烤纹理的相机
osg::ref_ptr<osg::Camera> camera = new osg::Camera; camera->setViewport(0, 0, tex_widht, tex_height); camera->setClearColor(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f)); camera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
相机视口的大小与纹理的大小相同。 另外,在清洁屏幕和清洁面罩时,请不要忘记设置背景颜色,这表示要同时清除颜色缓冲区和深度缓冲区。 接下来,配置相机以渲染为纹理
camera->setRenderOrder(osg::Camera::PRE_RENDER); camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); camera->attach(osg::Camera::COLOR_BUFFER, texture.get());
PRE_RENDER的渲染顺序指示此摄像机在渲染到主场景之前正在渲染。 将FBO指定为渲染目标,然后将纹理附加到相机。 现在,我们将相机设置为在绝对坐标系下工作,并在场景中设置我们要渲染为纹理的子树:附加了cessna模型的旋转变换
camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); camera->addChild(transform1.get());
通过添加主模型(正方形)和相机处理纹理来创建根组节点
osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(model.get()); root->addChild(camera.get());
创建和自定义查看器
osgViewer::Viewer viewer; viewer.setSceneData(root.get()); viewer.setCameraManipulator(new osgGA::TrackballManipulator); viewer.setUpViewOnSingleScreen(0);
设置摄像机的投影矩阵-通过裁剪金字塔的参数进行透视投影
camera->setProjectionMatrixAsPerspective(30.0, static_cast<double>(tex_widht) / static_cast<double>(tex_height), 0.1, 1000.0);
我们设置了一个视图矩阵,该矩阵设置了摄像机在空间中相对于子尾巴原点的位置
float dist = 100.0f; float alpha = 10.0f * 3.14f / 180.0f; osg::Vec3 eye(0.0f, -dist * cosf(alpha), dist * sinf(alpha)); osg::Vec3 center(0.0f, 0.0f, 0.0f); osg::Vec3 up(0.0f, 0.0f, -1.0f); camera->setViewMatrixAsLookAt(eye, center, up);
最后,制作动画并显示场景,在每个帧中更改飞机绕Z轴的旋转角度
float phi = 0.0f; float delta = -0.01f; while (!viewer.done()) { transform1->setMatrix(osg::Matrix::rotate(static_cast<double>(phi), osg::Vec3(0.0f, 0.0f, 1.0f))); viewer.frame(); phi += delta; }
结果,我们得到了一张相当有趣的图片

在此示例中,我们实现了一些场景动画,但是请记住,在组织访问不同流中的数据方面,扩展run()循环并在渲染帧之前或之后更改渲染参数是不安全的活动。 由于OSG使用多线程渲染,因此也存在用于在渲染过程中嵌入其自己的动作的常规机制,这些机制提供了对数据的线程安全访问。
6.将渲染结果保存到文件
OSG支持将osg :: Image对象附加到相机并将帧缓冲区的内容保存到图像数据缓冲区的功能。 之后,可以使用osg :: writeImageFile()函数将此数据保存到磁盘
osg::ref_ptr<osg::Image> image = new osg::Image; image->allocateImage( width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE ); camera->attach( osg::Camera::COLOR_BUFFER, image.get() ); ... osgDB::writeImageFile( *image, "saved_image.bmp" );
结论
也许本文中介绍的材料显得微不足道。 但是,它概述了在OpenSceneGraph中使用纹理的非常基本的知识,使用该引擎的更复杂的技术基于此,我们以后肯定会谈到。
待续...