Coin3D三维可视化教程7
使用灯光和照相机
在前面的章节中,我们介绍了组、属性、和形体等节点,并且演示了如何使用这些节点来创建场景。现在,我们将要学习可以影响到 3D 图形场景外观的两个节点类:灯光和照相机。在 Inventor 中,如同在现实世界里那样,灯光提供照明以便我们观察物体。如果在一个场景中没有包含任何灯光,并且当前的光照模型是缺省的 Phong lighting( 一种特定的光照计算公式,是 Bui Tuong Phong 于 1973 年发明的算法。译者注一种特定的光照计算公式,是 Bui Tuong Phong 于 1973 年发明的算法。译者注 ),那么场景中的物体都将处于黑暗中并且变得不可见。就像在现实世界中有多种光照类型那样-电灯泡、太阳、舞台灯-Inventor同样也提供了不同类型的灯光供我们在场景中使用。
照相机是我们观察场景的“眼睛”。Inventor 提供了一个和人类眼睛具有相同透视方式的照相机类。同时,Inventor 还另外提供另外一种用于产生场景 2D“快照”的照相机类,这种照相机使用另外一种类型的透视方式。本章将首先讨论照相机,并且假设在场景的最顶
端处至少存在一个灯光节点。
提示:观察器组件(Viewer components)将自动创建自己的照相机和灯光。更多内容请阅读第 16 章
照相机
照相机节点可以对场景中所有位于它之后的节点“拍摄”一张照片。因为照相机必须位于我们想要观察的物体之前,所以,通常要将照相机放在靠*场景最顶端的位置上。一个场景在同一时刻只能有一个激活的照相机。当前几何坐标变换将会影响照相机的空间位置。
SoCamera
所有照相机节点类都是从抽象基类 SoCamera 派生出来的。(见图 4-1)
SoCamera 具有下列域:
viewportMapping (SoSFEnum) | 用于处理当照相机镜头的横纵比与视口窗口的横纵比不同的情况下,如 何协调它们之间的映射关系.(详见“将照相机镜头的横纵比映射变换到 视口窗口的横纵比上”) |
position (SoSFVec3f) | 用于定位照相机的视点位置。当前几何变换可以修改这个位置。 |
orientation (SoSFRotation) ) | 照相机的观察方向。这个域用来描述照相机是如何相对于缺省方向进行 旋转的。缺省情况下,照相机的观察方向是从(0.0, 0.0, 1.0)的位置指向 坐标系原点,照相机的上方向(up direction)是(0.0,1.0, 0.0)。 这 个域连同当前几何变换可以指定照相机在世界坐标系下(又称全局坐标 系)的观察方向。 |
aspectRatio (SoSFFloat) | 照相机视口的宽高比.这个值必须大于 0 .在 SoCamera.h 中预定义了一 些宽高比数值: SO_ASPECT_SQUARE (1/1) SO_ASPECT_VIDEO (4/3) SO_ASPECT_HDTV (16/9) |
nearDistance (SoSFFloat) | 照相机的视点到*剪裁面的距离 |
farDistance (SoSFFloat) | 照相机的视点到远剪裁面的距离 |
focalDistance (SoSFFloat) | 照相机的视点到焦点的距离(通常用于 examiner 观察器) |
当进行渲染遍历时,如果遇到的是一个照相机节点,Inventor 将执行下列步骤:
1. 在执行渲染动作期间,首先在场景中定位照相机。(根据照相机的 position 和orientation 域指定照相机的位置和方向。同时修改当前的几何坐标变换也可以影响照相机的位置和方向)。
2. 照相机根据远*剪裁*面、横纵比、高度和高度角(依赖于照相机的类型)创建一个取景裁剪体(view volume)。取景裁剪体通常也叫做视图截锥(viewingfrustum),它是一个用来包围所要观察物体的六面锥台体。所有在取景裁剪体之外物体都将被剪裁丢弃。(本节的最后,有图表将演示不同类型的照相机是如何创建取景裁剪体的)。
3. 下一步,将 3D 取景裁剪体中的物体压缩映射到一张 2D 照片上,这个过程类似于使用光学照相机对真实世界进行拍摄的过程。然后将 2D 照片简单地映射到屏幕的 2D 窗口中 (见“将照相机的横纵比映射到视口上”)。
4. 接下来,使用照相机创建出来的投影矩阵来渲染图形场景的其余部分。
我们可以使用pointAt()方法来修改照相机orientation的域值。这个方法可以将照相机的方向指向一个特定的目标点。如果可能的话,它将尽可能保持照相机的“上方向”*行于+Y轴方向。否则将保持照相机的“上方向”*行于+Z轴方向。pointAt()方法的语法如下:
void pointAt(const SbVec3f &targetPoint )
SoCamera另外的两个常用方法是viewAll()和getViewVolume() 。viewAll()方法可以很容易地让照相机使用当前的方向来观察整个场景。这个方法需要提供被观察场景的根节点作为第一个参数(通常这个根节点要包含本照相机节点),以及需要提供渲染动作所要使用的视口区域(viewport region)作为第二个参数。slack参数通常用于定位远*剪裁*面。slack等于1.0(缺省值)将使远*剪裁*面“最紧密地包围住”(tightest fit)整个场景。viewAll()的语法如下:
void viewAll(SoNode *sceneRoot , const SbViewportRegion vpRegion, float slack = 1.0)
viewAll()方法会修改照相机的position 、nearDistance、farDistance域值。它不影响照相机的方向值。在“使用不同照相机观察场景”的章节中有如何使用viewAll()的例子。
getViewVolume()方法返回照相机的取景裁剪体,通常用于和拾取操作有关的功能。
SoCamera 的子类:
SoCamera类包括两个子类,如图 4-1 所示:
- SoPerspectiveCamera
- SoOrthographicCamera
SoPerspectiveCamera
SoPerspectiveCamera照相机类可以模拟人眼的功能:远处的物体变小,*处的物体变大。如果想模拟物体是怎样显示在人类的眼中,使用透视投影照相机是最自然不过的事情了。
SoPerspectiveCamera节点除了有SoCamera类所定义的所有域外,还另外附带一个域:heightAngle (SoSFFloat) 指定取景裁剪体的垂直高度角(弧度单位)
SoPerspectiveCamera节点所定义的取景裁剪体是一个如图 4-2 所示的截棱锥。高度角和横纵比按照下面的公式来决定宽度角:
widthAngle = heightAngle * aspectRatio
SoOrthographicCamera
相对于透视投影照相机,SoOrthographicCamera 类所代表的是*行投影(parallel projections)照相机。*行投影方式不会因为距离的原因而使图像发生变形。对于某些需要确保精度的设计工作,视觉的变形有可能对准确测量产生干扰。所以*行投影照相机对这类工作特别有用。
SoOrthographicCamera节点除了有SoCamera类所定义的所有域外,还另外有一个域:height (SoSFFloat) 指定取景裁剪体的高度。
SoOrthographicCamera 所定义的取景裁剪体是一个如图 4-3 所示的长方体。高度和横纵比按照下面的公式来决定矩形的宽度:
width = height * aspectRatio
将照相机的横纵比映射到视口上
视口(viewport)是窗口用于显示被渲染场景的矩形区。缺省情况下,视口和窗口(SoXtRenderArea) 的尺寸大小相同。当构造SoGLRenderAction (见第 9 章)对象时,需要指定视口作为构造函数的其中的一个参数。
SoCamera的viewportMapping域是用来指定当照相机的横纵比和视口不同时,如何将照相机的投影映射到视口上。前三个选项是通过修改视口来匹配照相机的投影。这三个选项的优点是照相机的横纵比保持不变 (缺点是视口可能会有空白区(dead space)出现)。
- CROP_VIEWPORT_FILL_FRAME 调整视口以匹配照相机。使用最适当的横纵比率来绘制视口。同时在没有用的地方填充上灰色。
- CROP_VIEWPORT_LINE_FRAME 调整视口以匹配照相机。使用框线来绘制视口的边界。
- CROP_VIEWPORT_NO_FRAME 调整视口以匹配照相机。不绘制视口的边界.
下面两个选项是调整照相机来匹配视口:
- ADJUST_CAMERA 调整照相机以匹配视口。照相机的投影图像是正常显示的,没有产生变形。(实际上是,保存在aspectRatio和height/heightAngle域中的数据是没有被修改的。如果视口映射需要的话,这些值只是临时地覆盖掉)。这个选项是缺省选项。
- LEAVE_ALONE 不修改任何数据。调整照相机的图像大小来匹配视口。这将有可能产生一个变形的图像。
使用不同类型的照相机观察场景
例 4-1 演示了在不同的位置上使用一个*行投影照相机和两个透视投影照相机来观察场景的代码。例子使用了一个频闪节点(blinker node)(见 13 章描述)来切换这三个照相机节点。场景(一个公园长椅)是从一个文件中读取的。图 4-5 显示了例子中图形场景的结构。
#include <Inventor/SbLinear.h>
#include <Inventor/SoDB.h>
#include <Inventor/SoInput.h>
#include <Inventor/Qt/SoQt.h>
#include <Inventor/Qt/SoQtRenderArea.h>
#include <Inventor/nodes/SoBlinker.h>
#include <Inventor/nodes/SoDirectionalLight.h>
#include <Inventor/nodes/SoMaterial.h>
#include <Inventor/nodes/SoOrthographicCamera.h>
#include <Inventor/nodes/SoPerspectiveCamera.h>
#include <Inventor/nodes/SoSeparator.h>
#include <Inventor/nodes/SoTransform.h>
int main(int argc, char* argv[]) {
// Initialize Inventor and Qt
QWidget *myWindow = SoQt::init(argv[0]);
if (myWindow ==NULL)
{
exit(1);
}
SoSeparator* root = new SoSeparator;
root->ref();
// Create a blinker node and put it in the scene. A blinker
// switches between its children at timed intervals.
SoBlinker* myBlinker = new SoBlinker;
root->addChild(myBlinker);
// Create three cameras.Their positions will be set later.
// This is because the viewAll method depends on the size
// of the render area, which has not been created yet.
SoOrthographicCamera * orthoViewAll = new SoOrthographicCamera;
SoPerspectiveCamera* perspViewAll = new SoPerspectiveCamera;
SoPerspectiveCamera* perspOffCenter = new SoPerspectiveCamera;
myBlinker->addChild(orthoViewAll);
myBlinker->addChild(perspViewAll);
myBlinker->addChild(perspOffCenter);
// Create a light
root->addChild(new SoDirectionalLight);
// Read the object from a file and add to the scene
SoInput myInput;
if (!myInput.openFile("parkbench.iv"))
return 1;
SoSeparator* fileContents = SoDB::readAll(&myInput);
if (fileContents == NULL)
return 1;
SoMaterial* myMaterial = new SoMaterial;
myMaterial->diffuseColor.setValue(0.8, 0.23, 0.03);
root->addChild(myMaterial);
root->addChild(fileContents);
SoQtRenderArea* myRenderArea = new SoQtRenderArea(myWindow);
// Establish camera positions.
// First do a viewAll() on all three cameras.
// Then modify the position of the off-center camera.
SbViewportRegion myRegion(myRenderArea->getSize());
orthoViewAll->viewAll(root, myRegion);
perspViewAll->viewAll(root, myRegion);
perspOffCenter->viewAll(root, myRegion);
SbVec3f initialPos;
initialPos = perspOffCenter->position.getValue();
float x, y, z;
initialPos.getValue(x, y, z);
perspOffCenter->position.setValue(x + x / 2., y + y / 2., z + z / 4.);
myRenderArea->setSceneGraph(root);
myRenderArea->setTitle("Cameras");
myRenderArea->show();
SoQt::show(myWindow);
SoQt::mainLoop();
return EXIT_SUCCESS;
}
灯光
当使用缺省光照模式时(Phong 模式),在可以观察物体之前,场景中必须包含至少一个灯光节点。在执行渲染动作期间,如果遍历遇到了场景中的灯光节点,渲染将开启这个灯光节点(During a rendering action, traversing a light node in the scene graph turns that light on.)。场景中灯光节点的位置可以决定两件事情:
- ? 灯光将照亮那些物体?- 在图形场景中,灯光节点可以照亮跟随它之后任何物体。(灯光参数是遍历状态的一部分(见第 3 章中的描述)。使用SoSeparator节点可以将一个特定灯光节点所产生的效果与图形场景中其它部分隔离开来 (即灯光节点如果是包含在一个即灯光节点如果是包含在一个SoSeparator 节点内, 则这个灯光节点只会照亮SoSeparator 节点内的物体。对于SoSeparator 节点外的物体将不会产生影响。译者注) )
- ? 灯光在 3D空间中位于何处? - 有些灯光节点(例如,SoPointLight)有一个location域。灯光的位置是受到当前几何坐标变换影响的。另外一些光源节点有一个direction域(例如,SoDirectionalLight),同样,灯光的方向也是受到当前几何坐标变换影响的。
关于所有光源节点的另一个重要事实是:灯光效果是累积的。每当向图形场景中增加一个灯光节点时,场景就会变得亮一些。Open Inventor 能开启的最大灯光数依赖于系统当前OpenGL 的具体实现。
SoLight
所有的灯光类都是从抽象基类SoLight派生出来的。SoLight是从SoNode派生出来的,它没有增加新的函数。SoLight有下列的域:
- on (SoSFBool) 灯光是否开启
- intensity (SoSFFloat) 灯光的亮度.数值的范围从 0.0(无光)到 1.0(最大亮度)
- color (SoSFColor) 灯光的颜色
SoLight 的子类
SoLight类有三个子类,如图 4-7:
-
? SoPointLight
-
? SoDirectionalLight
-
? SoSpotLight
图 4-8 演示了不同类型的灯光效果。左边的图表示的是光线的方向。右边的图显示了在相同场景下不同灯光类型渲染的效果。
提示:射灯(Directional lights)通常要比点光源(point lights)渲染的要快。它们俩同时又都比聚光灯(spotlights)渲染的要快。所以,如果想增加渲染速度的话,就要尽量使用少量、简单的灯光。
SoPointLight
SoPointLight灯光类,就像一颗星星那样,在给定的 3D空间位置上,均匀地向四周放射光线。SoPointLight节点有一个附加的域:location (SoSFVec3f) 点光源的 3D空间位置(这个位置是受到当前几何变换影响的)
SoDirectionalLight
SoDirectionalLight灯光类只是均匀地按照一个方向放射光线。因为它的光照距离是无限的,所以它不需要指定 3D空间位置。SoDirectionalLight节点有一个附加的域:direction (SoSFVec3f) 指定射灯发出光线的方向。(这个方向是受到当前几何变换影响的)。
提示:如果一个*面只是由一个多边形组成的(例如是一个大的矩形),并且这个多边形每个顶点的法向向量都相同,那么 Inventor 将不会显示出点光源的任何效果.这是因为灯光计算(使用 OpenGL)只是针对于每个顶点而言的。只有复杂的表面才能显示理想的效果.( 这里所说的点光源的效果,就是指当点光源照射一个由多个多边形组成的复杂物体时,在物体的表面会有一处是最亮的,在这个最亮处的周围会逐渐暗下来.而如果点光源照射的是只是一个多边形的话,受到 OpenGL 的限制,在这个多边形上就不会出现上述的效果,多边形依照与光源位置的相对关系,要么整个都是明的,要么整个都是暗的,不会出现中间过渡的部分.译者注)
使用多个灯光
现在我们可以试验向场景中增加不同类型的灯光。例 4-2 中包含有两个光源:一个固定位置的红色射灯源和一个绿色的点光源。绿色的点光源被 SoShuttle 节点(见 13 章)所控制,前后来回往复运动。图 4-10 给出了这个例子的整个场景图。
#include <Inventor/SoDB.h>
#include <Inventor/Qt/SoQt.h>
#include <Inventor/Qt/viewers/SoQtExaminerViewer.h>
#include <Inventor/nodes/SoCone.h>
#include <Inventor/nodes/SoDirectionalLight.h>
#include <Inventor/nodes/SoMaterial.h>
#include <Inventor/nodes/SoPointLight.h>
#include <Inventor/nodes/SoSeparator.h>
#include <Inventor/nodes/SoShuttle.h>
#include <Inventor/nodes/SoTransformSeparator.h>
int main(int argc, char* argv[]) {
// Initialize Inventor and Qt
QWidget *myWindow = SoQt::init(argv[0]);
if (myWindow == NULL)
exit(1);
SoSeparator* root = new SoSeparator;
root->ref();
// Add a directional light
SoDirectionalLight* myDirLight = new SoDirectionalLight;
myDirLight->direction.setValue(0, -1, -1);
myDirLight->color.setValue(1, 0, 0);
root->addChild(myDirLight);
// Put the shuttle and the light below a transform separator.
// A transform separator pushes and pops the transformation
// just like a separator node, but other aspects of the state
// are not pushed and popped. So the shuttle's translation
// will affect only the light. But the light will shine on
// the rest of the scene.
SoTransformSeparator* myTransformSeparator =
new SoTransformSeparator;
root->addChild(myTransformSeparator);
// A shuttle node translates back and forth between the two
// fields translation0 and translation1.
// This moves the light.
SoShuttle* myShuttle = new SoShuttle;
myTransformSeparator->addChild(myShuttle);
myShuttle->translation0.setValue(-2, -1, 3);
myShuttle->translation1.setValue(1, 2, -3);
// Add the point light below the transformSeparator
SoPointLight* myPointLight = new SoPointLight;
myTransformSeparator->addChild(myPointLight);
myPointLight->color.setValue(0, 1, 0);
root->addChild(new SoCone);
SoQtExaminerViewer* myViewer =
new SoQtExaminerViewer(myWindow);
myViewer->setSceneGraph(root);
myViewer->setTitle("Lights");
myViewer->setHeadlight(FALSE);
myViewer->show();
SoQt::show(myWindow);
SoQt::mainLoop();
}