今天课程分为两部分,第一部分我们学习一下Scene Graph理论知识,第二部分我们熟悉下OSG的源码。
第一部分(Scene Graph)
在OpenSceneGraph中,场景图(Scene Graph)通过树状层级结构高效管理3D对象。
场景图(Scene Graph)层级关系
以下是根节点、组节点和几何节点(Geode)的核心概念及层级关系:
根节点 (osg::Group)
│
└── 变换组节点 (osg::PositionAttitudeTransform)
│
├── 几何节点1 (osg::Geode) → 包含立方体
│
└── 子组节点 (osg::Group)
│
└── 几何节点2 (osg::Geode) → 包含球体
根节点(Root Node)
作用:场景图的顶层入口,所有其他节点均为其子孙节点。
类型:通常是osg::Group或osgViewer::Viewer关联的根节点。
特性:
- 无父节点。
- 作为场景遍历的起点,渲染时从根节点开始递归处理子节点。
组节点(Group Node)
作用:组织子节点,构建层次结构。支持嵌套,用于组合变换、状态或逻辑分组。
类型:基类为osg::Group,扩展类型包括osg::Transform(变换节点)、osg::Switch(开关节点)、osg::LOD(细节层次节点)等。
特性:
通过addChild()方法添加子节点(其他组节点或Geode)。
- 示例:一个“汽车”组节点可包含“车轮”、“车身”等子组节点,每个子组可进一步细分。
几何节点(Geode)
作用:叶子节点,保存实际几何数据(如顶点、法线、纹理坐标)。
类型:osg::Geode(Geometry Node)。
特性:
- 无子节点,通过addDrawable()添加osg::Drawable对象(如osg::Geometry)。
- 示例:一个Geode节点可包含立方体或球体的几何数据。
看的是不是云里雾里的,伟大的圣人王阳明说过,要知行合一,所以下面我们通过代码来实践。
代码实例
scene_graph.cpp
#include <osg/Geode>
#include <osg/Group>
#include <osg/ShapeDrawable>
#include <osgViewer/Viewer>
#include <osg/Material>
#include <osg/StateSet>
int main()
{
// 创建根节点
osg::ref_ptr<osg::Group> root = new osg::Group();
// 创建第一个组节点(红色方块组)
osg::ref_ptr<osg::Group> redGroup = new osg::Group();
// 创建第一个几何节点(红色方块)
osg::ref_ptr<osg::Geode> geode1 = new osg::Geode();
geode1->addDrawable(new osg::ShapeDrawable(new osg::Box(osg::Vec3(-2,0,0), 1.0f)));
geode1->getOrCreateStateSet()->setAttribute(new osg::Material());
osg::Material* material1 = dynamic_cast<osg::Material*>(geode1->getStateSet()->getAttribute(osg::StateAttribute::MATERIAL));
if (material1)
{
material1->setDiffuse(osg::Material::FRONT, osg::Vec4(1,0,0,1)); // 红色
}
// 创建第二个组节点(蓝色方块组)
osg::ref_ptr<osg::Group> blueGroup = new osg::Group();
// 创建第二个几何节点(蓝色方块)
osg::ref_ptr<osg::Geode> geode2 = new osg::Geode();
geode2->addDrawable(new osg::ShapeDrawable(new osg::Box(osg::Vec3(2,0,0), 1.0f)));
geode2->getOrCreateStateSet()->setAttribute(new osg::Material());
osg::Material* material2 = dynamic_cast<osg::Material*>(geode2->getStateSet()->getAttribute(osg::StateAttribute::MATERIAL));
if (material2)
{
material2->setDiffuse(osg::Material::FRONT, osg::Vec4(0,0,1,1)); // 蓝色
}
// 构建场景图层级关系
root->addChild(redGroup); // 根节点包含红色组
root->addChild(blueGroup); // 根节点包含蓝色组
redGroup->addChild(geode1); // 红色组包含几何体1
blueGroup->addChild(geode2); // 蓝色组包含几何体2
// 创建查看器并设置场景数据
osgViewer::Viewer viewer;
viewer.setSceneData(root.get());
return viewer.run();
}
代码都有注释,还是很好理解的。
接下来就是编译文件CMakeLists.txt
:
cmake_minimum_required(VERSION 3.12)
project(OSG_SceneGraph_Demo)
# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 查找OpenSceneGraph核心组件
find_package(OpenSceneGraph REQUIRED
COMPONENTS
osg # 核心库
osgDB # 文件读写
osgViewer # 查看器功能
osgGA # 图形上下文
osgUtil # 工具库
)
# 包含头文件路径
include_directories(
${OPENSCENEGRAPH_INCLUDE_DIR}
)
# 创建可执行文件
add_executable(${PROJECT_NAME} scene_graph.cpp)
# 链接OpenSceneGraph库
target_link_libraries(${PROJECT_NAME}
${OPENSCENEGRAPH_LIBRARIES}
# Windows需要额外链接
$<$<PLATFORM_ID:Windows>:OpenThreads>
)
# 配置调试模式
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(${PROJECT_NAME} PRIVATE DEBUG)
message(STATUS "Building in DEBUG mode")
endif()
运行效果
第二部分(源码解析)
看完了上面的实例,我们来看下源码的执行。
从上面代码我们可以看到,最后都是要走到view
类的run
函数。
我们来看下这个函数都干了什么?
我这里下载的是OpenSceneGraph-3.2.3版本。
首先找到源码的Viewer
(OsgViewer/osgViewer.cpp)类:
int Viewer::run()
{
if (!getCameraManipulator() && getCamera()->getAllowEventFocus())
{
setCameraManipulator(new osgGA::TrackballManipulator());
}
setReleaseContextAtEndOfFrameHint(false);
return ViewerBase::run();
}
这里实现和简单就是跳转了ViewerBase::run()
.
继续跟踪分析:
int ViewerBase::run()
{
......
while(!done() && (run_frame_count_str==0 || getViewerFrameStamp()->getFrameNumber()<runTillFrameNumber))
{
.......
frame();
.......
}
return 0;
}
最后就执行到了ViewerBase::frame()
函数。
进入这个函数我们看下它主要干了啥。
void ViewerBase::frame(double simulationTime)
{
if (_done) return;
// OSG_NOTICE<<std::endl<<"CompositeViewer::frame()"<<std::endl<<std::endl;
if (_firstFrame)
{
viewerInit();
if (!isRealized())
{
realize();
}
_firstFrame = false;
}
advance(simulationTime);
eventTraversal();
updateTraversal();
renderingTraversals();
}
我们一个一个来分析:
第一步:初始化
if (_firstFrame)
{
viewerInit();
if (!isRealized())
{
realize();
}
_firstFrame = false;
}
viewerInit():
初始化相机、场景等核心组件。
realize():
创建原生窗口并绑定 OpenGL 上下文(若未就绪)。
如果这是仿真系统启动后的第一帧,则执行viewerInit()
;此时如果还没有执行realize()
函数,则执行它。
第二步:推进场景状态
advance(simulationTime);
作用:调用场景中所有节点的 advance() 方法。
参数:simulationTime 通常表示逻辑时间(用于动画/物理模拟)。
第三步:事件处理
eventTraversal();
从窗口系统获取输入事件(键盘、鼠标等)。
通过 osgGA::EventQueue 分发事件。
触发事件处理器(osgGA::EventHandler)的回调。
第四步:场景更新
updateTraversal();
调用所有节点的 update() 回调。
更新场景图状态(位置变化、LOD切换等)。
执行 osg::NodeCallback 自定义更新逻辑。
第五步: 渲染遍历
renderingTraversals();
核心步骤:
裁剪(Cull):确定可见对象,生成渲染列表。
绘制(Draw):提交 OpenGL 命令到 GPU。
交换缓冲区:显示渲染结果(swapBuffers())。
嗯~,今天的学习需要消化消化,下课,明天见。_
参考文献
《最长的一帧》王锐(array)