目录
引言
本文为我学习OSG库的笔记,其目的是在已有计算机图形学的基础上,记录OSG的关键知识,以达到快速学习的目的。
教材:OSG-DOC.pdf。
第一章:OSG概述
一、前言
(1)为什么要学习OSG?
我在本科毕业时尝试使用DirectX去设计一个通用图形库,它旨在为Windows上应用程序提供图像计算和渲染服务,使得程序开发者不需要了解计算机图形学以及图像API,就能渲染出满意的图形。
在设计过程中,学习DirectX12并不是我遇到最难的问题,而是如何设计通用图形库的架构,如何构建通用的Shader接口?如何提供和Shader配套的设置?如何组织场景对象?这些是需要思考的问题。
OSG场景图像系统是使用OpenGL开发的库,它能让程序员更快速、便捷地创建高性能和跨平台的交互式图像程序。也许你会用OpenGL,但你是否能够设计出通用、高性能、跨平台的OpenGL封装库呢?
无论是工作要使用OSG,还是学习如何设计图形引擎甚至游戏引擎,学习OSG都将使你受益匪浅。
(2)OSG的组成
在系统的底层绘图硬件和相应的软件驱动程序之上,OSG封装了OpenGL。
OSG由多个模块组成,它主要包括如下4个库(通读一遍下文即可)。
(3)OSG的智能指针
OSG提供了智能指针类osg::ref_ptr来管理内存并防止内存泄漏,智能指针使用示例如下。
// 创建场景浏览器实例osgViewer::Viewer
osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer(); // 推荐写法
osgViewer::Viewer* viewer = new osgViewer::Viewer(); // 不推荐写法
(4)OSG的安装编译
二、第一个OSG程序
(1)Hello OSG程序
程序代码如下,本文所有代码具有注释,非关键点不再赘述。
#include<osgViewer/Viewer>
#include<osg/Node>
#include<osg/Geode>
#include<osg/Group>
#include<osgDB/ReadFile>
#include<osgDB/WriteFile>
#include<osgUtil/Optimizer>
int main()
{
// 创建场景浏览器实例osgViewer::Viewer
osg::ref_ptr<osgViewer::Viewer> viewer
= new osgViewer::Viewer();
// 创建一个场景组节点osg::Group
osg::ref_ptr<osg::Group> root
= new osg::Group();
// 创建一个节点osg::node,并将牛模型读入到此节点中
osg::ref_ptr<osg::Node> node
= osgDB::readNodeFile("cow.osg");
// 将node节点加入为group节点的子节点
root->addChild(node.get());
// 优化场景数据结构
osgUtil::Optimizer optimizer;
optimizer.optimize(root.get());
// 将group节点设置为场景浏览器的场景数据
viewer->setSceneData(root.get());
// 初始化并创建窗口
viewer->realize();
// 开始渲染
viewer->run();
return 0;
}
代码的学习,建议大家首先照着代码和注释打一遍,然后理解注释和代码,再然后删掉注释按照理解自己重写注释,最后删掉原代码按照注释还原代码。(找不到cow.osg文件的安装OSG-DATA并配置到环境变量,若使用了vcpkg可能会忽略环境变量,可在vs中进行设置)
(2)OSG渲染程序的基本流程
根据上述程序,可知OSG场景渲染程序的基本流程如下:
步骤 | 内容 |
---|---|
1 | 创建场景浏览器,即通过osgViewer::Viewer类创建对象,用于渲染场景 |
2 | 加载模型和场景数据 |
3 | 建立场景树,确定场景数据之间的关系 |
4 | 执行渲染场景的循环 |
OSG提供许多丰富功能可使用命令行使用,本文专注于图像库的使用和设计,因此不再叙述,详情可看教材原文。
第二章:数学基础
虽然学习过计算机图形学,但学习OSG是怎样对图形学系统进行封装的,是非常有必要的。
OSG中的向量类有如下种类。
一、坐标系统
坐标系是什么?坐标系是一个精确定位对象位置的框架,所有的图形变换都是基于一定的坐标系进行的。
常见坐标系有世界坐标系、物体坐标系和摄像机坐标系。
(1)世界坐标系
世界坐标系又称为全局坐标系,它描述的是整个场景中的所有对象,它为所有对象的位置提供一个绝对的参考标准,可以理解为绝对坐标系,因为所有对象的位置都是绝对坐标。
(2)物体坐标系
物体坐标系是针对某一特定的物体建立的独立坐标系,它使得描述单独物体非常方便,比如建模师可能会在空间原点附近建模一个人体,这个人体模型就位于物体坐标系。
建模师通常不在乎模型会被放到世界的哪个角落,它只需要在物体坐标系下建模好人物,然后生成人物的多个动画,当3D开发者使用时将模型变换到世界坐标即可。
(3)摄像机坐标系
摄像机坐标系是和观察者相关的坐标系。摄像机坐标系和屏幕坐标系类似,但二者的差异在于摄像机坐标系处于3D空间中,而屏幕坐标系在2D平面里。
摄像机坐标系描述的问题是:“哪些物体应该渲染并显示在屏幕上?”,主要包括物体是否在摄像机坐标系区域内、物体的渲染顺序和物体的遮挡剔除。
OSG和OpenGL的世界坐标系都是左手坐标系,并且X轴都是向右,但OpenGL的Y轴向上且Z轴向你(即垂直指向屏幕外),而OSG的Z轴向上且Y轴垂直屏幕向里,具体见下图。
二、坐标系变化
(1)物体坐标系-世界坐标系变化
三维实体对象需要经过一系列的坐标变化才能正确、真实地显示在屏幕上。
每个物体对象都定义在自己的物体坐标系下,当渲染时,每个物体对象通过变化矩阵变换到世界坐标系中。
如何在OSG中实现从物体坐标系到世界坐标系呢?OSG以节点组成场景树,每个节点都有自己的父节点和自己的变化矩阵,变化矩阵记录了如何从自己的坐标系变化到父节点坐标系,因此只需将该节点与根节点之间所有节点的变化矩阵相乘即可。
如何实现上述遍历和计算过程呢?在OSG中有多种方式,如回调、访问器等。用访问器的好处是方便可控,每一帧都会自动计算矩阵变化,但缺点是回调在一定程度上不可操控,并且会增加额外开销而影响渲染效率。
(2)访问器
访问器通过遍历的方式记录场景中节点的路径,并根据路径上的变化矩阵计算出世界坐标。下面以代码的形式展示访问器的使用,可将略看一遍。
#include<osgViewer/Viewer>
#include<osg/Node>
#include<osg/Geode>
#include<osg/Group>
#include<osgDB/ReadFile>
#include<osgDB/WriteFile>
#include<osgUtil/Optimizer>
// 手工如何计算?从节点到根节点,将变换矩阵逐个相乘计算最终结果。
// 定义新的节点访问器类,以实现对节点和场景树的自定义形式访问
// 新访问器类需要继承osg::NodeVisitor
class GetWorldCoordinateOfNodeVisitor : public osg::NodeVisitor
{
public:
// 节点访问器类的构造函数,需要初始化osg::NodeVisitor
// NodeVisitor::TRAVERSE_PARENTS表示访问目标节点和其父节点
GetWorldCoordinateOfNodeVisitor() :
osg::NodeVisitor(NodeVisitor::TRAVERSE_PARENTS), done(false)
{
/*
osg::ref_ptr主要用于自动管理那些继承自osg::Referenced类的对象,
而osg::VecN和osg::MatrixT类似整数和浮点数直接使用即可,
但是osg::VecN*和osg::MatrixT*搭配new申请空间时,需要自己手动释放,否则会造成内存泄漏
*/
wcMatrix = new osg::Matrixd();
}
// 自定义访问节点和场景树的方式
virtual void apply(osg::Node& node)
{
// done标识是否遍历到根节点,就像手算一样若遍历到根节点则逐层回退
if (!done)
{
// 虽然说场景是树,但其实它是一个无环图,因为一个节点可能有多个父节点
// 若一个场景需要渲染多棵同样的树,让树节点被多个具有不同变换的父节点引用即可
if (0 == node.getNumParents())
{
// 若没有父节点则到达场景根节点,计算最终世界坐标并标识根节点已到达
wcMatrix->set(osg::computeLocalToWorld(this->getNodePath()));
done = true;
}
traverse(node);
}
}
// 要返回最终变换矩阵的地址,因此该类没有处理osg::Matrixd可能造成的内存泄漏
osg::Matrixd* giveUpDaMat()
{
return wcMatrix;
}
private:
bool done;
osg::Matrix* wcMatrix;
};
// 访问节点node计算其最终变换矩阵
osg::Matrixd* getWorldCoords(osg::Node* node)
{
/*
* 若使用osg::ref_ptr<osg::node>作为参数,则该函数可能使得计数增加,使得node永远不能释放,造成内存泄漏
* 因此若一个函数不应记录某参数,则应向其传入osg::T*即类型指针,并因此要检查此指针是否为空
*/
// 创建自定义访问器对象,由于其被节点所引用,因此应使用new申请方式
GetWorldCoordinateOfNodeVisitor* ncv = new GetWorldCoordinateOfNodeVisitor();
if (node && ncv)
{
// 将访问器应用到节点,节点会引用该访问器
node->accept(*ncv);
// 返回访问器的遍历结果
return ncv->giveUpDaMat();
}
else
{
return NULL;
}
}
(3)坐标变换总结
模型顶点数据存在于物体坐标系中,要将模型变换到最终的屏幕上,需要让顶点数据经过的变换过程为:物体坐标系(模型变换)-> 世界坐标系(观察投影变换)->裁剪空间(视口变换)->屏幕坐标系。
模型和投影变换将顶点数据变换到归一化的设备坐标系中,最后再有视口变换得到屏幕窗口坐标。