转自:http://blog.sina.com.cn/s/blog_4b46937b01018i2x.html
在场景中创建两个视口。其中一个用于从坦克驾驶员的视角观察场景。该视口将被渲染于屏幕的上半部分。第二个视口由缺省的osgViewer::Viewer类接口(轨迹球,飞行等控制器)控制。它将被渲染于屏幕的中下部分。
概述:
OSG向开发人员提供了各种的抽象层次接口。前面的教程讨论的主要是一些较高层级的接口应用:例如使用Viewer类来控制视点,场景,交互设备和窗口系统。OSG的优势之一,就是可以允许开发者在使用高层次的接口的同时,访问较低层次的抽象接口。本章将使用一些低抽象层级的功能,对视点进行控制,并使用相应的类渲染场景。
代码:
为了创建两个视口,我们需要提供两个独立可控的摄像机。与OSG 1.2版本中所述不同的是,本例中将不再使用Prodecer::CameraConfig类,而是将多个不同的视口添加到组合视口 CompositeViewer类当中。下面的函数即用于实现添加视口并设置其中的摄像机位置。
void createView (osgViewer::CompositeViewer *viewer,//查看器,一个相框
osg::ref_ptr<osg::Group> scene,//场景
osg::ref_ptr<osg::GraphicsContext> gc,//显示设置定义相框的大小,View和Viewr在屏幕上的大小,位置
osgGA::TrackballManipulator* Tman,//放置相机的位置
int x, int y, int width, int height)//视口的大小,从多大的窗口看场景
{
double left,right,top,bottom,near,far, aspectratio;
double frusht, fruswid, fudge;
bool gotfrustum = true;
// 向最终的组合视口添加一个新的视口,并设置其操控方式。
//View是一个显示窗口
osgViewer::View* view = new osgViewer::View;
//在查看器中加入显示窗口
viewer->addView(view);
//显示窗口设定自身相机的位置
view->setCameraManipulator(Tman);
// 设置视口的场景数据,并设置摄像机的截锥坐标。
//指定场景
view->setSceneData(scene.get());
//指定视口的大小,通过个窗口的大小来查看场景,也就是相片的大小
view->getCamera()->setViewport(new osg::Viewport(x,y, width,height));
//指定相机的焦距
view->getCamera()-> getProjectionMatrixAsFrustum(left,right,
bottom,top,
near,far);
if (gotfrustum)
{
aspectratio = (double) width/ (double) height;
frusht = top - bottom;
fruswid = right - left;
fudge = frusht*aspectratio/fruswid;
right = right*fudge;
left = left*fudge;
view->getCamera()-> setProjectionMatrixAsFrustum(left,right,
bottom,top,
near,far);
}
//指定相机中的图像在屏幕上的显示方法,与系统相关
view->getCamera()->setGraphicsContext(gc.get());// 添加渲染状态控制器
//这个控制器,控制图的光照,材质等
osg::ref_ptr<osgGA::StateSetManipulator> statesetManipulator = new osgGA::StateSetManipulator;
statesetManipulator->setStateSet(view->getCamera()->getOrCreateStateSet());
view->addEventHandler( statesetManipulator.get() );
}
现在我们已经有了设置摄像机的函数,在仿真的其余部分中,我们将不再赘述有关基本场景建立(包括一个地形模型以及在其上运动的坦克)的内容。相关的代码可以从源程序中获取。我们需要对坦克模型添加一个位移变换节点。这样我们就可以将摄像机的位置置于坦克的后上访,以便进行观察。
int main( int argc, char **argv )
{
// 场景根节点和坦克模型节点指针。
osg::ref_ptr<osg::Group> rootNode;
osg::ref_ptr<osg::Group> ownTank;
osgGA::TrackballManipulator *Tman1 = new osgGA::TrackballManipulator();
osgGA::TrackballManipulator *Tman2 = new osgGA::TrackballManipulator();
// 建立场景和坦克。
if (!setupScene(rootNode, ownTank))
{
std::cout<< "problem setting up scene" << std::endl;
return -1;
}
// 声明一个位于坦克偏后上方的位移变换节点。将其添加到坦克节点。
osg::PositionAttitudeTransform * followerOffset =
new osg::PositionAttitudeTransform();
followerOffset->setPosition( osg::Vec3(0.0,-25.0,10) );
followerOffset->setAttitude(
osg::Quat( osg::DegreesToRadians(-15.0), osg::Vec3(1,0,0) ) );
ownTank.get()->addChild(followerOffset);
// 声明一个自定义的位移累加器类,以便放置相机。将其关联给上面的变换节点。
tankFollowerWorldCoords->attachToGroup(followerOffset);
// 构建视口类,以及与其相关的图形设备类。
osgViewer::CompositeViewer viewer;
//得到屏幕接口
osg::GraphicsContext::WindowingSystemInterface* wsi =
osg::GraphicsContext::getWindowingSystemInterface();
if (!wsi)
{
osg::notify(osg::NOTICE)
<<"Error, no WindowSystemInterface available, cannot create windows."<<std::endl;
return 1;
}
unsigned int width, height;
//屏幕的分辨率
wsi->getScreenResolution(osg::GraphicsContext::ScreenIdentifier(0), width, height);
//设置图形上下文的属性
osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits;
//x,y定义了viewer的起始位置,width和height定义了Viewer(显示相框)的大小
traits->x = 100;
traits->y = 100;
traits->width = width;
traits->height = height;
traits->windowDecoration = true;
traits->doubleBuffer = true;
traits->sharedContext = 0;
osg::ref_ptr<osg::GraphicsContext> gc = osg::GraphicsContext::createGraphicsContext(traits.get());
if (gc.valid())
{
osg::notify(osg::INFO)<<" GraphicsWindow has been created successfully."<<std::endl;
gc->setClearColor(osg::Vec4f(0.2f,0.2f,0.6f,1.0f));
gc->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
else
{
osg::notify(osg::NOTICE)<<" GraphicsWindow has not been created successfully."<<std::endl;
}
// 第一个视口
createView (&viewer,rootNode,gc,Tman1,0, 0, traits->width/2, traits->height);
// 第二个视口
createView (&viewer,rootNode,gc,Tman2,traits->width/2, 0, traits->width/2, traits->height);
viewer.setThreadingModel(osgViewer::CompositeViewer::SingleThreaded);
现在我们已经基本建立了仿真的代码。它与前述的程序有少许不同。在每次更新场景图形节点之后,我们都会手动重置控制器的位置,从而设置摄像机的方向和跟随效果。另一个视口的摄像机则保持缺省的位置和接口不变。代码如下所示:
while( !viewer.done() )
{
// 获取跟踪摄像机的句柄。使用相应的方法设置坦克跟踪相机的世界坐标位置矩阵。
// 注意该矩阵需要从Y轴向上旋转到Z轴向上的坐标系。
Tman2->setByInverseMatrix(tankFollowerWorldCoords->getMatrix()
*osg::Matrix::rotate( -M_PI/2.0, 1, 0, 0 ));
viewer.frame();
}return 0;
}
Camera::setViewPort决定了相片也就是图像的大小。
osg::GraphicsContext::Traits的Width,Height 定义了在屏幕上显示的相框的大小。
一般ViewPort和相框会一样大,如果ViewPort大于相框,则在相框中只显示一部分的场景。
如果ViewPort小于相框,相框中会一部分的空白。
在OSG中,使用Camera得到图像,并通过设置其中的GraphicsContext显示到屏幕上
。在View和Viewer中都有一个或多个Camera,实现对场景的显示和对GUI事件的
反应。
关于其中的概念解释:
摘自OpenGL 基础图形编程-OpenGL变换8
相机模型:在真实世界里,所有的物体都是三维的。但是,这些三维物体在计算机世界中却必须以二维平面物体的形式表现出来。
那么,这些物体是怎样从三维变换到二维的呢?下面我们采用相机(Camera)模拟的方式来讲述这个概念,
如下图所示:
实际上,从三维空间到二维平面,就如同用相机拍照一样,通常都要经历以下几个步骤 (括号内表示的是相应的图形学概念):
第一步,将相机置于三角架上,让它对准三维景物(视点变换,Viewing Transformation)。
第二步,将三维物体放在适当的位置(模型变换,Modeling Transformation)。
第三步,选择相机镜头并调焦,使三维物体投影在二维胶片上(投影变换,Projection Transformation)。
第四步,决定二维像片的大小(视口变换,Viewport Transformation)。
这样,一个三维空间里的物体就可以用相应的二维平面物体表示了,也就能在二维的电脑屏幕上正确显示了。
三维图形显示流程
运用相机模拟的方式比较通俗地讲解了三维图形显示的基本过程,但在具体应用OpenGL函数库编程时,还必须了解三维图形世界中的几个特殊坐标系的概念,
以及用这些概念表达的三维图形显示流程。
计算机本身只能处理数字,图形在计算机内也是以数字的形式进行加工和处理的。大家都知道,坐标建立了图形和数字之间的联系。
为了使被显示的物体数字化,要在被显示的物体所在的空间中定义一个坐标系。
这个坐标系的长度单位和坐标轴的方向要适合对被显示物体的描述,这个坐标系称为世界坐标系。
计算机对数字化的显示物体作了加工处理后,要在图形显示器上显示,这就要在图形显示器屏幕上定义一个二维直角坐标系,这个坐标系称为屏幕坐标系。
这个坐标系坐标轴的方向通常取成平行于屏幕的边缘,
坐标原点取在左下角,长度单位常取成一个象素的长度,大小可以是整型数。
为了使显示的物体能以合适的位置、大小和方向显示出来,必须要通过投影。投影的方法有两种,即正射投影和透视投影。
有时为了突出图形的一部分,只把图形的某一部分显示出来,这时可以定义一个三维视景体(Viewing Volume)。
正射投影时一般是一个长方体的视景体,透视投影时一般是一个棱台似的视景体。
只有视景体内的物体能被投影在显示平面上,其他部分则不能。
在屏幕窗口内可以定义一个矩形,称为视口(Viewport),视景体投影后的图形就在视口内显示。
为了适应物理设备坐标和视口所在坐标的差别,还要作一适应物理坐标的变换。这个坐标系称为物理设备坐标系。
根据上面所述,三维图形的显示流程应如图8-2所示。
1)视点变换。视点变换是在视点坐标系中进行的。相当于放置相机的位置。
对应MatrixManipulator类
2)模型变换。模型变换是在世界坐标系中进行的。相当于布景。在这个坐标系中,
可以对物体实施平移glTranslatef()、旋转glRotatef()和放大缩小glScalef()。
对应PositionAttitudeTransform类 和 MatrixTransform类,
3)投影变换。投影变换类似于选择相机的镜头。本例中调用了一个透视投影函数glFrustum(),
在调用它之前先要用glMatrixMode()说明当前矩阵方式是投影GL_PROJECTION。
这个投影函数一共有六个参数,由它们可以定义一个棱台似的视景体。即视景体内的部分可见,
视景体外的部分不可见,这也就包含了三维裁剪变换。
3.1正射投影(Orthographic Projection)
正射投影,又叫平行投影。这种投影的视景体是一个矩形的平行管道,也就是一个长方体,
如下图所示。正射投影的最大一个特点是无论物体距离相机多远,投影后的物体大小尺寸不变。
这种投影通常用在建筑蓝图绘制和计算机辅助设计等方面,这些行业要求投影后的物体尺寸
及相互间的角度不变,以便施工或制造时物体比例大小正确。
OpenGL正射投影函数共有两个,这在前面几个例子中已用过。一个函数是:
void glOrtho(GLdouble left,GLdouble right,GLdouble bottom,GLdouble top,
GLdouble near,GLdouble far)
在OSG中:
void osg::Camera::setProjectionMatrixAsOrtho
(double left, double right, double bottom, double top, double zNear, double zFar)
它创建一个平行视景体。实际上这个函数的操作是创建一个正射投影矩阵,并且用这个
矩阵乘以当前矩阵。其中近裁剪平面是一个矩形,矩形左下角点三维空间坐标是(left,bottom,-near),
右上角点是(right,top,-near);远裁剪平面也是一个矩形,左下角点空间坐标是(left,bottom,-far
),右上角点是(right,top,-far)。所有的near和far值同时为正或同时为负。
如果没有其他变换,正射投影的方向平行于Z轴,且视点朝向Z负轴。
这意味着物体在视点前面时far和near都为负值,物体在视点后面时far和near都为正值。
另一个函数是:
void gluOrtho2D(GLdouble left,GLdouble right,GLdouble bottom,GLdouble top)
在OSG中
void setProjectionMatrixAsOrtho2D
(double left, double right, double bottom, double top)
它是一个特殊的正射投影函数,主要用于二维图像到二维屏幕上的投影。它的near和far缺省值
分别为-1.0和1.0,所有二维物体的Z坐标都为0.0。
因此它的裁剪面是一个左下角点为(left,bottom)、右上角点为(right,top)的矩形。
3.2透视投影(Perspective Projection)
透视投影符合人们心理习惯,即离视点近的物体大,离视点远的物体小,远到极点即为消失,成为灭点。
它的视景体类似于一个顶部和底部都被切除掉的棱椎,也就是棱台。
这个投影通常用于动画、视觉仿真以及其它许多具有真实性反映的方面。
OpenGL透视投影函数也有两个,其中函数glFrustum()在8.1.3节中提到过,它所形成的视景体如图8-10所示。
这个函数原型为:
void glFrustum(GLdouble left,GLdouble Right,GLdouble bottom,GLdouble top,
GLdouble near,GLdouble far);
在OSG中为:
void osg::Camera::setProjectionMatrixAsFrustum
(double left, double right, double bottom, double top, double zNear, double zFar)
它创建一个透视视景体。其操作是创建一个透视投影矩阵,并且用这个矩阵乘以当前矩阵。这个函数的参数
只定义近裁剪平面的左下角点和右上角点的三维空间坐标,即(left,bottom,-near)和(right,top,-near);
最后一个参数far是远裁剪平面的Z负值,其左下角点和右上角点空间坐标由函数根据透视投影原理自动生成。
near和far表示离视点的远近,它们总为正值。
另一个函数是:
void gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear, GLdouble zFar);
在OSG中
void setProjectionMatrixAsPerspective
(double fovy, double aspectRatio, double zNear, double zFar)
它也创建一个对称透视视景体,但它的参数定义于前面的不同,如图8-11所示。其操作是创建一个对称的透视投影矩阵,
并且用这个矩阵乘以当前矩阵。
参数fovy定义视野在X-Z平面的角度,范围是[0.0, 180.0];参数aspect是投影平面宽度与高度的比率;参数zNear和Far分别
是远近裁剪面沿Z负轴到视点的距离,它们总为正值。
以上两个函数缺省时,视点都在原点,视线沿Z轴指向负方向。
4)视口变换。视口变换就是将视景体内投影的物体显示在二维的视口平面上。通常,都调用函数glViewport()来定义一个视口,
这个过程类似于将照片放大或缩小。
用法。运用相机模拟方式,我们很容易理解视口变换就是类似于照片的放大与缩小。在计算机图形学中,它的定义是将经过几何变换
、投影变换和裁剪变换后的物体显示于屏幕窗口内指定的区域内,
这个区域通常为矩形,称为视口。OpenGL中相关函数是:
glViewport(GLint x,GLint y,GLsizei width, GLsizei height);
在OSG中
void setViewport (int x, int y, int width, int height)
这个函数定义一个视口。函数参数(x, y)是视口在屏幕窗口坐标系中的左下角点坐标,参数width和height分别是
视口的宽度和高度。缺省时,参数值即(0, 0, winWidth, winHeight) 指的是屏幕窗口的实际尺寸大小。
所有这些值都是以象素为单位,全为整型数。
注意:在实际应用中,视口的长宽比率总是等于视景体裁剪面的长宽比率。如果两个比率不相等,那么投影后的图像显示于
视口内时会发生变形,如图8-14所示。另外,屏幕窗口的改变一般不明显影响视口的大小。
因此,在调用这个函数时,最好实时检测窗口尺寸,及时修正视口的大小,保证视口内的图像能随窗口的变化而变化,且不变形。