深入理解OpenSceneGraph(三):渲染管线与状态管理
本文是「深入理解OpenSceneGraph」系列文章的第三篇,将深入剖析OSG的渲染管线与状态管理系统——从StateSet状态集到StateAttribute状态属性,从CullVisitor拣选遍历到RenderBin渲染排序,揭示OSG高效渲染的内部机制。
一、OSG渲染架构概览
1.1 从场景图到像素:渲染的完整流程
对于大多数3D引擎初学者来说,最困惑的问题之一就是:我在场景图里放了一堆节点,它们最终是怎么变成屏幕上的像素的?这个问题看似简单,实则涉及引擎最核心的渲染架构。
OSG的渲染过程可以概括为三个主要阶段:更新遍历(Update Traversal)、拣选遍历(Cull Traversal)和绘制遍历(Draw Traversal)。这三个阶段依次执行,共同完成一帧的渲染。
第一阶段:更新遍历(Update)
更新遍历是每一帧的开始。在这个阶段,OSG从根节点开始遍历整个场景图,执行每个节点的更新回调(UpdateCallback)。更新回调通常用于:
- 移动物体、播放动画
- 更新游戏逻辑、AI状态
- 处理输入事件的结果
- 修改场景图结构(添加/删除节点)
更新遍历的本质是:在渲染之前,让应用程序有机会更新场景的状态。
第二阶段:拣选遍历(Cull)
拣选遍历是OSG最核心、最复杂的阶段。它的主要任务是:
- 视锥剔除:判断哪些节点在视锥体内,哪些在外面
- 状态收集:从根节点到叶子节点,收集并合并所有的渲染状态
- 变换累积:累积每个节点的变换矩阵,得到世界坐标
- 生成渲染叶子:将可见的Drawable包装成RenderLeaf
- 渲染排序:按照状态和深度对RenderLeaf进行排序
- 构建渲染图:最终生成一棵StateGraph和多个RenderBin
拣选遍历的输出是一个"渲染图"(Render Graph),它已经不是场景图了,而是按照渲染效率优化过的数据结构。
第三阶段:绘制遍历(Draw)
绘制遍历是最简单也最"重"的阶段。它遍历拣选阶段生成的渲染图,依次调用OpenGL API执行实际的绘制命令。这个阶段的工作主要是:
- 按照状态图设置OpenGL状态
- 按照RenderBin的顺序绘制
- 执行每个Drawable的draw()方法
绘制遍历的时间主要花在GPU上,CPU只是负责下达命令。
1.2 为什么需要这三个阶段
你可能会问:为什么不直接一边遍历场景图一边绘制?为什么要分成三个阶段?
答案是:为了性能。
1. 状态排序
如果直接遍历并绘制,绘制顺序就是场景图的遍历顺序。这意味着OpenGL状态会频繁切换——可能刚设置了一个材质,下一个物体又要换成另一个材质。状态切换在GPU上是比较昂贵的操作。
通过先拣选再绘制,我们可以将使用相同状态的物体排在一起绘制,大大减少状态切换的次数。
2. 透明物体排序
透明物体需要从后往前绘制才能正确混合。如果直接遍历绘制,无法保证这个顺序。拣选阶段可以对透明物体进行深度排序。
3. 多线程
三阶段的设计使得多线程成为可能。比如,更新和拣选可以在CPU上进行,同时GPU在执行上一帧的绘制命令。
4. 剔除优化
在拣选阶段就可以把不可见的物体剔除掉,根本不需要送到绘制阶段。这比送到GPU再由GPU裁剪要高效得多。
1.3 帧循环(Frame Loop)
OSG的帧循环由osgViewer::ViewerBase类管理。每一帧的大致流程是:
while (!done)
{
// 1. 事件处理
viewer->eventTraversal();
// 2. 更新遍历
viewer->updateTraversal();
// 3. 拣选+绘制
viewer->renderingTraversals();
// (可选)等待垂直同步
}
在多线程模式下,拣选和绘制可能由不同的线程执行,情况会更复杂一些。我们将在多线程章节详细讨论。
二、StateSet状态集深度解析
2.1 什么是StateSet
osg::StateSet(状态集)是OSG状态管理的核心。它可以看作是一组渲染状态的集合——包括材质、纹理、着色器、光照、混合模式等等。场景图中的每个节点都可以关联一个StateSet,用于控制该节点及其子节点的渲染方式。
StateSet的设计体现了OSG的一个核心思想:将状态与几何分离。几何数据(顶点、法线等)由Drawable管理,渲染状态由StateSet管理。两者在Geode中结合起来。
class OSG_EXPORT StateSet : public Object
{
public:
StateSet();
StateSet(const StateSet& ss, const CopyOp& copyop = CopyOp::SHALLOW_COPY);
// 设置/获取状态属性
void setAttribute(StateAttribute* attribute,
unsigned int mode = OVERRIDE | PROTECTED);
StateAttribute* getAttribute(TypeAttributePair typePair);
// 设置/获取纹理属性
void setTextureAttribute(unsigned int unit, StateAttribute* attribute,
unsigned int mode = OVERRIDE | PROTECTED);
StateAttribute* getTextureAttribute(unsigned int unit,
TypeAttributePair typePair);
// 设置/获取模式(GL_LIGHTING、GL_DEPTH_TEST等)
void setMode(GLenum mode, unsigned int stateMode = ON | OVERRIDE | PROTECTED);
bool getMode(GLenum mode) const;
// 设置纹理模式
void setTextureMode(unsigned int unit, GLenum mode,
unsigned int stateMode = ON | OVERRIDE | PROTECTED);
// 添加/获取Uniform变量
void addUniform(Uniform* uniform,
unsigned int mode = OVERRIDE | PROTECTED);
Uniform* getUniform(const std::string& name) const;
// 渲染提示(透明、遮罩等)
void setRenderingHint(RenderingHint hint);
RenderingHint getRenderingHint() const;
enum RenderingHint {
DEFAULT_BIN, // 默认渲染顺序
OPAQUE_BIN, // 不透明物体
TRANSPARENT_BIN // 透明物体(需要从后往前排序)
};
// 渲染Bin详情
void setRenderBinDetails(int binNum, const std::string& binName);
int getBinNumber() const;
const std::string& getBinName() const;
// ... 更多方法
};
2.2 状态的继承与覆盖
StateSet最重要的特性之一是状态继承——子节点会继承父节点的状态。但这种继承不是简单的复制,而是"合并":子节点的StateSet会和父节点的StateSet合并,子节点的设置会覆盖父节点中相同的项。
这种机制使得我们可以在高层节点设置全局状态(如全局光照),在低层节点覆盖特定的状态(如某个物体使用特殊的材质)。
状态属性的覆盖模式:
每个状态属性都有一个"模式值",控制它如何与父状态合并:
enum Values {
OFF = 0x00, // 关闭该状态
ON = 0x01, // 开启该状态
OVERRIDE = 0x2, // 覆盖子节点的设置(父节点强制)
PROTECTED = 0x4,// 保护本节点不被父节点覆盖
INHERIT = 0x8 // 继承自父节点
};
这些值可以组合使用:
ON:启用该属性,如果父节点也有同名属性,默认会被子节点覆盖ON | OVERRIDE:启用该属性,并且强制子节点也使用这个设置(子节点无法覆盖)ON | PROTECTED:启用该属性,并且不被父节点的OVERRIDE影响OFF:禁用该属性OFF | OVERRIDE:禁用该属性,并强制子节点也禁用
这是一个非常强大的机制。比如,你可以在根节点设置光照为ON | OVERRIDE,这样所有子节点都会启用光照,即使某些子节点自己设置了关闭光照也没用。
状态合并的过程:
当OSG在拣选遍历中从父节点进入子节点时,会进行状态合并。对于每个状态属性:
- 如果子节点的属性有
PROTECTED标志,则使用子节点的属性 - 否则,如果父节点的属性有
OVERRIDE标志,则使用父节点的属性 - 否则,如果子节点有该属性,则使用子节点的属性
- 否则,使用父节点的属性
这个规则确保了状态的正确合并,同时提供了足够的灵活性。
2.3 状态属性(StateAttribute)
osg::StateAttribute是所有状态属性的基类。OSG内置了大量的状态属性子类,覆盖了OpenGL渲染状态的方方面面。
主要的状态属性包括:
| 属性类别 | 代表类 | 功能 |
|---|---|---|
| 材质 | Material | 环境光、漫反射、高光、发射光颜色,光泽度 |
| 光照模型 | LightModel | 全局光照模型设置 |
| 纹理 | Texture2D, TextureCubeMap等 | 纹理图像与纹理参数 |
| 着色器 | Program, Shader, Uniform | GLSL着色器程序 |
| 混合 | BlendFunc, BlendColor, BlendEquation | 颜色混合模式 |
| 深度 | Depth | 深度测试与深度写入 |
| 模板 | Stencil, StencilTwoSided | 模板缓冲 |
| 裁剪 | Scissor | 裁剪测试 |
| 颜色遮罩 | ColorMask | 颜色通道写入控制 |
| 多边形模式 | PolygonMode | 点/线/面模式 |
| 多边形偏移 | PolygonOffset | 深度偏移(解决z-fighting) |
| 点 | Point | 点大小 |
| 线 | LineWidth, LineStipple | 线宽、线样式 |
| 雾 | Fog | 雾化效果 |
| 着色模型 | ShadeModel | 光滑/平面着色 |
| 剪裁平面 | ClipPlane | 附加剪裁平面 |
每个StateAttribute子类都封装了一组相关的OpenGL状态。比如Material封装了glMaterialfv()相关的所有参数。
使用示例:
osg::ref_ptr<osg::StateSet> ss = node->getOrCreateStateSet();
// 设置材质
osg::ref_ptr<osg::Material> mat = new osg::Material;
mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f));
mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f));
mat->setShininess(osg::Material::FRONT_AND_BACK, 64.0f);
ss->setAttribute(mat.get());
// 启用光照
ss->setMode(GL_LIGHTING, osg::StateAttribute::ON);
// 设置纹理
osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D;
texture->setImage(osgDB::readImageFile("texture.jpg"));
ss->setTextureAttributeAndModes(0, texture.get(), osg::StateAttribute::ON);
// 启用混合(透明效果)
ss->setMode(GL_BLEND, osg::StateAttribute::ON);
ss->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
2.4 纹理状态
纹理是一种特殊的状态属性——因为OpenGL支持多个纹理单元(Texture Unit),所以纹理状态是"按单元"管理的。
StateSet提供了专门的纹理属性方法:
// 设置纹理属性和模式(最常用的方法)
void setTextureAttributeAndModes(unsigned int unit, StateAttribute* sa,
unsigned int mode = ON | OVERRIDE | PROTECTED);
// 只设置纹理属性
void setTextureAttribute(unsigned int unit, StateAttribute* sa,
unsigned int mode = OVERRIDE | PROTECTED);
// 只设置纹理模式
void setTextureMode(unsigned int unit, GLenum mode, unsigned int stateMode);
// 获取纹理属性
StateAttribute* getTextureAttribute(unsigned int unit, TypeAttributePair typePair);
纹理单元的编号从0开始。在固定管线中,通常支持2-4个纹理单元;在可编程管线中,可以有更多。
纹理参数:
Texture类封装了纹理图像和纹理参数。常用的纹理参数包括:
- 过滤方式:
setFilter(Texture::MIN_FILTER, Texture::LINEAR_MIPMAP_LINEAR) - 环绕方式:
setWrap(Texture::WRAP_S, Texture::REPEAT) - 边框颜色:
setBorderColor(color) - 纹理大小:可以设置内部格式等
Mipmap生成:
OSG可以自动生成Mipmap。只需要设置min filter为使用mipmap的模式,OSG会在纹理第一次上传到GPU时自动生成mipmap。
texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR_MIPMAP_LINEAR);
texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR);
2.5 Uniform变量
在可编程管线(Shader)中,Uniform变量是CPU向GPU传递数据的主要方式。OSG的osg::Uniform类封装了Uniform变量。
// 创建Uniform变量
osg::ref_ptr<osg::Uniform> timeUniform =
new osg::Uniform("time", 0.0f); // 名字+值
// 添加到StateSet
stateset->addUniform(timeUniform.get());
// 在更新回调中修改值
timeUniform->set(currentTime);
Uniform支持多种数据类型:
- 基本类型:
float,int,bool,double,uint - 向量类型:
vec2,vec3,vec4,ivec3等 - 矩阵类型:
mat2,mat3,mat4 - 数组类型
Uniform变量是按名字匹配的——在GLSL中声明的uniform变量,只要名字和类型匹配,OSG就会自动绑定。
内置Uniform变量:
OSG还会自动提供一些内置的Uniform变量,比如:
osg_FrameNumber:当前帧号osg_FrameTime:当前帧时间osg_DeltaFrameTime:帧间隔时间osg_ViewMatrix:视图矩阵osg_ProjectionMatrix:投影矩阵osg_ModelViewMatrix:模型视图矩阵- 等等
这些内置Uniform使得在着色器中获取常用矩阵和时间信息变得非常方便。
2.6 渲染Bin与透明排序
StateSet中的RenderBin(渲染箱/渲染桶)控制着物体的渲染顺序。这对于透明物体的渲染尤其重要。
为什么透明物体需要特殊处理?
在OpenGL中,透明物体的渲染需要满足两个条件:
- 要启用混合(
GL_BLEND) - 透明物体必须从后往前绘制(先画远的,再画近的)
这是因为透明混合的公式是:结果 = 源颜色 * 源alpha + 目标颜色 * (1 - 源alpha)。如果先画近的再画远的,远处的物体就无法透过近处的物体"看到",因为近处的物体已经把深度缓冲写死了。
OSG的RenderBin机制:
OSG将渲染分为多个"Bin",每个Bin有一个编号和排序方式:
| Bin名称 | 编号 | 排序方式 | 用途 |
|---|---|---|---|
StateGraphBin | -2 | 按状态排序 | 不透明物体(默认) |
DepthSortedBin | 10 | 按深度从远到近排序 | 透明物体 |
RenderBin | - | 可自定义 | 自定义渲染顺序 |
默认情况下,物体在StateGraphBin中,按状态排序(以减少状态切换)。当你设置setRenderingHint(TRANSPARENT_BIN)时,物体就会被放到DepthSortedBin中,按深度从远到近排序。
自定义RenderBin:
你也可以创建自定义的RenderBin,实现特殊的渲染顺序。比如,你可能希望某些UI元素最后渲染(在所有物体之上):
// UI放在最后渲染,bin number设为100
stateset->setRenderBinDetails(100, "RenderBin");
RenderBin的编号越小,越先渲染;编号越大,越后渲染。
三、着色器与可编程管线
3.1 从固定管线到可编程管线
早期的OpenGL使用"固定管线"(Fixed Function Pipeline)——你只能通过开关一些选项、设置一些参数来控制渲染效果,而不能自定义渲染算法。这就像一台只有几个旋钮的电视机,你只能调亮度、对比度,不能改电路。
随着GPU的发展,"可编程管线"(Programmable Pipeline)逐渐取代了固定管线。在可编程管线中,你可以编写自己的着色器程序(Shader),在顶点处理和片元处理阶段插入自定义逻辑。这大大增强了渲染的灵活性。
OSG从2.0版本开始支持着色器,到3.x版本已经非常成熟。OSG的着色器系统设计得相当优雅,它将GLSL着色器封装成Shader和Program对象,可以像其他状态属性一样设置到StateSet中。
3.2 Shader与Program
osg::Shader代表单个着色器对象(如顶点着色器、片元着色器),osg::Program代表完整的着色器程序(由多个Shader链接而成)。
// 创建顶点着色器
osg::ref_ptr<osg::Shader> vertexShader = new osg::Shader(
osg::Shader::VERTEX,
"void main() { \n"
" gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; \n"
" gl_TexCoord[0] = gl_MultiTexCoord0; \n"
"} \n"
);
// 创建片元着色器
osg::ref_ptr<osg::Shader> fragmentShader = new osg::Shader(
osg::Shader::FRAGMENT,
"uniform sampler2D texture0; \n"
"void main() { \n"
" gl_FragColor = texture2D(texture0, gl_TexCoord[0].st); \n"
"} \n"
);
// 创建Program并链接着色器
osg::ref_ptr<osg::Program> program = new osg::Program;
program->addShader(vertexShader.get());
program->addShader(fragmentShader.get());
// 应用到StateSet
stateset->setAttribute(program.get());
Shader类型:
OSG支持多种类型的着色器:
| 类型 | 说明 |
|---|---|
VERTEX | 顶点着色器 |
FRAGMENT | 片元着色器 |
GEOMETRY | 几何着色器 |
TESS_CONTROL | tessellation控制着色器 |
TESS_EVALUATION | tessellation评估着色器 |
COMPUTE | 计算着色器 |
从文件加载着色器:
在实际项目中,通常不会把着色器代码写在C++代码里,而是存放在单独的文件中。OSG提供了osgDB::readShaderFile()函数来加载着色器文件:
osg::ref_ptr<osg::Program> program = new osg::Program;
program->addShader(osgDB::readShaderFile(osg::Shader::VERTEX, "shader.vert"));
program->addShader(osgDB::readShaderFile(osg::Shader::FRAGMENT, "shader.frag"));
3.3 着色器组合器(ShaderComposer)
在复杂的应用中,你可能有很多种效果需要不同的着色器组合。比如,有的物体需要基础纹理+光照,有的需要纹理+法线贴图+光照,有的需要骨骼动画+纹理+光照等等。如果每种组合都写一个完整的着色器,代码量会非常大,而且难以维护。
OSG的osg::ShaderComposer提供了一种"模块化着色器"的解决方案。它允许你将着色器代码拆分成多个功能模块(函数),然后在运行时根据需要组合成完整的着色器。
ShaderComposer的基本思想是:
- 定义一组"着色器组件",每个组件实现一个功能(如Phong光照、法线贴图、骨骼动画等)
- 定义组件之间的依赖关系
- 在运行时,根据物体的状态自动选择需要的组件,组合成完整的着色器
这种方式类似于Unity的ShaderGraph或UE的材质编辑器,只不过是代码层面的。
虽然ShaderComposer功能强大,但它的学习曲线也比较陡峭。对于简单的项目,可能不需要用到它。
3.4 虚拟程序(VirtualProgram)
osg::VirtualProgram是另一种着色器管理机制,它的灵感来自于OpenGL的固定管线。VirtualProgram允许你设置"默认"的顶点/片元着色器,然后在子节点中"替换"其中的某些部分。
VirtualProgram的核心思想是:提供一套标准的着色器钩子(Hook),子类可以重写这些钩子函数来实现自定义效果,而不需要重写整个着色器。
比如,标准的顶点着色器可能有这些钩子函数:
void vertexTransform():顶点变换void normalTransform():法线变换void texCoordTransform():纹理坐标变换void lighting():光照计算
你可以在子节点中只替换lighting()函数来实现自定义光照,而其他部分保持不变。
VirtualProgram在需要对不同物体做相似但略有不同的渲染时非常有用。
四、CullVisitor拣选遍历详解
4.1 拣选遍历的角色
如果说更新遍历是"逻辑",绘制遍历是"输出",那么拣选遍历就是连接两者的"大脑"。拣选遍历是OSG最复杂、最核心的部分,它的效率直接决定了引擎的整体性能。
拣选遍历的主要任务可以概括为:从场景图中找出所有可见的物体,并按照高效的方式组织起来,交给绘制阶段去渲染。
这个"高效的方式"包括:
- 状态排序:相同状态的物体放在一起,减少状态切换
- 深度排序:透明物体从后往前绘制
- 状态合并:将父节点和子节点的状态合并
- 变换累积:计算每个物体的世界坐标
4.2 CullVisitor类结构
osgUtil::CullVisitor是拣选遍历的核心类,它继承自NodeVisitor。在拣选过程中,CullVisitor维护着一系列的"状态栈":
class OSGUTIL_EXPORT CullVisitor : public NodeVisitor
{
public:
CullVisitor();
// 各种节点的apply方法
virtual void apply(Node& node);
virtual void apply(Geode& node);
virtual void apply(Group& node);
virtual void apply(Transform& node);
virtual void apply(LOD& node);
virtual void apply(Switch& node);
virtual void apply(Camera& camera);
// ...
// 获取当前的模型视图矩阵、投影矩阵等
const Matrix& getModelViewMatrix() const;
const Matrix& getProjectionMatrix() const;
// 渲染状态栈
State* getState() const;
// 当前的RenderStage(渲染阶段)
RenderStage* getCurrentRenderStage();
// 添加一个RenderLeaf到当前RenderBin
void addDrawableAndDepth(Drawable* drawable, StateSet* ss, float depth);
// ...
};
4.3 状态栈与矩阵栈
CullVisitor内部维护着多个栈结构,用于跟踪从根节点到当前节点的累积状态:
1. 矩阵栈
矩阵栈用于累积变换矩阵。每进入一个Transform节点,就把它的矩阵乘到栈顶;离开时再弹出。
栈底:单位矩阵(根节点)
→ Group1(不变换)
→ Transform1(矩阵A)→ 栈顶变为 A
→ Group2
→ Transform2(矩阵B)→ 栈顶变为 A * B
→ Geode(此时的模型矩阵是 A * B)
← Transform2 弹出B,栈顶恢复为A
← Transform1 弹出A,栈顶恢复为单位矩阵
栈顶回到单位矩阵
2. 状态栈
状态栈用于累积渲染状态。每进入一个有StateSet的节点,就把它的状态合并到当前状态;离开时恢复之前的状态。
状态栈的实现不是简单地保存完整的StateSet(那样太浪费了),而是使用StateGraph(状态图)来高效地管理状态的继承关系。我们将在下一节详细讨论StateGraph。
3. 其他栈
除了矩阵栈和状态栈,CullVisitor还维护着:
- 视锥体栈:用于裁剪
- 灯光栈:管理灯光
- 裁剪平面栈:管理裁剪平面
- 等等
4.4 视锥剔除(Frustum Culling)
视锥剔除是拣选遍历最重要的功能之一。它的基本思想很简单:如果一个物体完全在视锥体(相机可见的区域)之外,就不需要渲染它。
视锥体由六个平面定义:
- 左平面、右平面
- 上平面、下平面
- 近平面、远平面
剔除算法:
对于每个节点,CullVisitor会执行以下步骤:
- 获取节点的局部包围球
- 将包围球变换到视图空间(或者将裁剪平面变换到局部空间)
- 检测包围球与视锥体六个平面的位置关系
- 根据检测结果决定:
- 完全在外面:跳过该节点及其所有子节点
- 完全在里面:不需要再检查子节点(所有子节点肯定也在里面),直接全部加入渲染队列
- 相交:需要继续检查子节点
这个过程是递归的,形成了一个"剔除树"。对于平衡的场景图,剔除的时间复杂度是O(log n)。
包围体与平面的相交检测:
包围球与平面的相交检测是一个非常基础但重要的几何运算。对于一个平面ax + by + cz + d = 0和一个包围球(中心点P,半径r):
- 计算球心到平面的有符号距离:
distance = a*P.x + b*P.y + c*P.z + d - 如果
distance > r:球完全在平面的正面(视锥内侧) - 如果
distance < -r:球完全在平面的负面(视锥外侧) - 否则:球与平面相交
对于视锥体来说,只要包围球在任何一个裁剪平面的外侧(远的那一侧),就可以认为整个物体不可见。这就是所谓的"平面外测试"(Outside-in Test)。
注意,这个测试可能会有误判——有时候包围球虽然和所有平面都相交,但实际上不在视锥体内(比如在视锥体的某个角外面)。这种情况叫做"假阳性"。但没关系,最多就是多画一点东西,不会导致错误。真正需要避免的是"假阴性"——把可见的物体当成不可见的剔除掉了。
4.5 LOD选择
在拣选遍历中,遇到LOD节点时,CullVisitor需要根据距离选择合适的子节点。
LOD选择的步骤:
- 计算LOD节点中心到相机的距离(或屏幕像素大小)
- 根据距离查找对应的子节点索引
- 只遍历选中的那个子节点(其他子节点被跳过)
这个过程是在CullVisitor::apply(LOD& lod)中实现的。
对于PagedLOD,如果选中的子节点还没加载,CullVisitor会向DatabasePager提交加载请求,并使用当前可用的最高精度层级(通常是最近的已加载层级)作为替代。
4.6 生成RenderLeaf
当CullVisitor遍历到一个Geode节点时,它不会直接绘制,而是将每个Drawable包装成一个RenderLeaf,添加到当前的RenderBin中。
struct RenderLeaf
{
Drawable* _drawable; // 可绘制对象
StateSet* _stateset; // 最终合并后的状态集
float _depth; // 深度值(用于排序)
// ... 其他信息
};
RenderLeaf是绘制阶段的基本单位。每个RenderLeaf代表一个"绘制命令"——用什么状态,画什么东西。
RenderLeaf会被放到当前的RenderBin中。根据Bin类型的不同,它们会被以不同的方式排序:
- StateGraphBin:按状态排序
- DepthSortedBin:按深度排序
五、状态图(StateGraph)与渲染排序
5.1 为什么需要状态排序
在讨论StateGraph之前,让我们先思考一个问题:为什么要对渲染命令进行排序?
假设场景中有100个物体,其中50个使用材质A,50个使用材质B。如果物体的顺序是交替的(A、B、A、B、A、B...),那么GPU需要在两个材质之间切换100次。状态切换在GPU上是比较昂贵的操作,频繁切换会严重影响性能。
如果我们把物体重新排序,把所有使用材质A的放在前面,所有使用材质B的放在后面,那么只需要切换一次状态。这可以大幅提升性能。
这就是状态排序的意义:通过减少OpenGL状态切换的次数,提高渲染效率。
5.2 StateGraph状态图
osgUtil::StateGraph是OSG实现状态排序的核心数据结构。它是一棵"状态树"——从根节点到叶子节点的路径代表一组完整的渲染状态。
StateGraph的结构大致如下:
Root (空状态)
├── 状态A(材质1)
│ ├── 状态A+B(材质1+纹理1)
│ │ └── Leaf... (使用材质1+纹理1的物体)
│ └── 状态A+C(材质1+纹理2)
│ └── Leaf...
└── 状态D(材质2)
└── Leaf...
每个StateGraph节点代表"在父状态的基础上,再添加一些状态"。这样,相同的状态前缀可以被共享,避免重复。
当添加一个新的RenderLeaf时,OSG会:
- 将该物体的完整状态集"分解"成一系列的状态变更
- 从StateGraph的根节点开始,沿着匹配的路径往下走
- 如果某个状态变更是当前节点没有的,就创建一个新的子节点
- 最后把RenderLeaf放到对应的叶子节点
StateGraph的优势:
-
状态共享:相同的状态前缀只需要设置一次。渲染时,沿着StateGraph的路径走,每进入一个子节点就应用它的状态变更,不需要每次都重新设置完整的状态。
-
自动排序:由于相同状态的物体被放在同一个StateGraph节点下,渲染时自然就是按状态分组的。
-
高效合并:场景图中的状态继承关系可以很自然地映射到StateGraph的层次结构中。
5.3 渲染遍历的执行
当拣选遍历完成后,就得到了一棵完整的StateGraph(以及其他RenderBin)。接下来的绘制遍历就是遍历这棵StateGraph,执行渲染命令。
绘制遍历的过程大致如下:
function drawStateGraph(node):
// 应用当前节点的状态变更
applyStateChanges(node.stateChanges)
// 绘制当前节点的所有RenderLeaf
for each leaf in node.leaves:
leaf.drawable.draw()
// 递归绘制所有子节点
for each child in node.children:
drawStateGraph(child)
// 恢复状态变更(弹出状态)
undoStateChanges(node.stateChanges)
这个过程就像深度优先遍历一棵树。进入节点时应用状态,离开时恢复状态。这样,状态的变化次数等于StateGraph的边数,而不是RenderLeaf的数量。
对于有大量物体但状态种类不多的场景,这种方式可以极大地减少状态切换次数。比如,1000个物体如果只有10种不同的状态组合,那么状态切换次数只有几十次,而不是上千次。
5.4 RenderBin的层次结构
实际上,渲染的组织结构比单纯的StateGraph更复杂。OSG使用RenderBin的层次结构来管理不同类型的渲染顺序。
整个结构大致是:
RenderStage(渲染阶段,对应一个Camera)
├── preRenderStages(预渲染的子Camera)
├── StateGraphBin(不透明物体,状态排序,编号-2)
│ └── StateGraph...
├── 其他自定义Bin...
├── DepthSortedBin(透明物体,深度排序,编号10)
│ └── RenderLeaf列表(按深度排序)
└── postRenderStages(后渲染的子Camera)
RenderStage对应一个Camera的渲染结果。如果有多个Camera(比如主相机+镜面反射相机+HUD相机),就会形成RenderStage的嵌套结构。
每个RenderStage包含多个RenderBin,按编号从小到大依次渲染。不透明物体在低编号的Bin中,先渲染;透明物体在高编号的Bin中,后渲染。
这种设计非常灵活,可以支持各种复杂的渲染需求。
六、osgViewer视图系统
6.1 Viewer与View
osgViewer是OSG的应用框架层,它封装了渲染循环、窗口管理、相机操纵等功能,使得开发者可以快速搭建一个3D查看应用。
osgViewer的核心类包括:
Viewer:单视图查看器,最常用CompositeViewer:多视图查看器,可以有多个ViewView:视图,对应一个相机和一个场景GraphicsWindow:图形窗口,封装了操作系统的窗口CameraManipulator:相机操纵器,处理用户输入控制相机
Viewer的基本使用:
#include <osgViewer/Viewer>
#include <osgDB/ReadFile>
int main()
{
// 创建查看器
osgViewer::Viewer viewer;
// 加载模型并设置为场景数据
viewer.setSceneData(osgDB::readNodeFile("model.osgt"));
// 设置相机操纵器(默认是轨迹球操纵器)
viewer.setCameraManipulator(new osgGA::TrackballManipulator);
// 运行主循环
return viewer.run();
}
这短短几行代码就创建了一个完整的3D模型查看器——可以用鼠标旋转、缩放、平移视角。
6.2 渲染的三种线程模型
OSG支持多种线程模型,可以通过setThreadingModel()设置:
viewer.setThreadingModel(osgViewer::Viewer::SingleThreaded);
1. SingleThreaded(单线程)
所有操作(事件、更新、拣选、绘制)都在一个线程中顺序执行。
优点:简单,容易调试 缺点:性能较差,CPU和GPU不能并行工作
适用场景:调试、简单应用、单CPU核心机器
2. CullDrawThreadPerContext(每个上下文一个线程)
每个图形上下文(窗口)有一个线程,负责拣选和绘制。更新和事件在主线程中执行。
这是最常用的线程模型。在这种模式下:
- 主线程:处理事件、更新遍历
- 渲染线程:拣选遍历 + 绘制遍历
优点:CPU和GPU可以一定程度上并行 缺点:拣选和绘制还是串行的
3. CullThreadPerCameraDrawThreadPerContext(每个相机一个拣选线程,每个上下文一个绘制线程)
这是最高级的线程模型。每个相机有自己的拣选线程,每个图形上下文有自己的绘制线程。
在这种模式下:
- 主线程:处理事件、更新遍历
- 拣选线程(每个相机一个):拣选遍历
- 绘制线程(每个上下文一个):绘制遍历
优点:最高的并行度,充分利用多核CPU 缺点:最复杂,可能有更多的同步开销
适用场景:多相机、多GPU、CPU核心很多的情况
线程模型的选择:
对于大多数应用,CullDrawThreadPerContext是一个不错的选择。它在性能和复杂度之间取得了较好的平衡。
如果你的场景比较简单,或者主要瓶颈在GPU而不是CPU,那么单线程模式可能就足够了。
如果你的场景非常复杂,CPU是瓶颈,并且有多个CPU核心可用,可以尝试使用多线程模式。
6.3 相机操纵器(CameraManipulator)
相机操纵器负责将用户的鼠标/键盘输入转换为相机的运动。OSG提供了多种相机操纵器:
| 操纵器 | 用途 |
|---|---|
TrackballManipulator | 轨迹球,围绕一个中心点旋转(最常用) |
OrbitManipulator | 轨道式,类似轨迹球但有更多功能 |
FirstPersonManipulator | 第一人称,类似FPS游戏的控制方式 |
FlightManipulator | 飞行模式,模拟飞行器操纵 |
DriveManipulator | 驾驶模式,模拟汽车驾驶 |
UFOManipulator | UFO模式,平滑的飞行控制 |
TerrainManipulator | 地形模式,适合地形浏览 |
SphericalManipulator | 球面模式,适合浏览球体(如地球) |
NodeTrackerManipulator | 节点跟踪,跟随某个节点运动 |
AnimationPathManipulator | 动画路径,沿预设路径运动 |
KeySwitchMatrixManipulator | 按键切换,可以在多个操纵器之间切换 |
自定义相机操纵器:
你也可以通过继承osgGA::CameraManipulator来实现自己的相机控制方式。只需要实现几个关键方法:
setByMatrix()/setByInverseMatrix():设置相机矩阵getMatrix()/getInverseMatrix():获取相机矩阵handle():处理事件
相机操纵器虽然名字叫"操纵器",但它的本质就是一个"根据用户输入计算相机矩阵"的组件。理解了这一点,自定义操纵器就不难了。
6.4 GraphicsWindow图形窗口
osgViewer::GraphicsWindow是OSG对操作系统窗口的抽象。它封装了不同平台的窗口创建、事件处理等功能。
OSG支持多种窗口系统:
- Windows:Win32窗口
- Linux:X11窗口
- macOS:Cocoa窗口
- iOS:iOS窗口
- Android:Android窗口(通过NDK)
- 嵌入式:各种嵌入式窗口系统
通常情况下,你不需要直接和GraphicsWindow打交道,Viewer会帮你创建和管理窗口。但在某些特殊情况下(比如嵌入到MFC/Qt/SDL等框架中),你可能需要手动创建GraphicsWindow。
嵌入到其他GUI框架:
OSG可以很容易地嵌入到其他GUI框架中。基本思路是:
- 创建一个GUI窗口/控件
- 获取它的窗口句柄(HWND、Window ID等)
- 创建OSG的GraphicsWindow,指定使用这个窗口句柄
- 将Viewer的相机设置为使用这个GraphicsWindow
具体的嵌入方法因框架而异。OSG自带了MFC、Qt、SDL、FLTK、FOX、GTK、WxWidgets等多种框架的嵌入示例。
七、多通道与多窗口渲染
7.1 多相机渲染
在OSG中,一个Viewer可以有多个Camera(通过View管理,或者直接作为场景图的子节点)。每个Camera从不同的角度渲染场景,结果可以显示在屏幕的不同位置,或者作为纹理用于其他效果。
多相机渲染的应用场景包括:
- 多视口显示:比如三视图(前视、侧视、俯视)+ 透视图
- HUD(平视显示器):2D的UI元素,使用正交投影
- 镜面反射:将场景渲染到纹理,然后贴到镜子上
- 阴影:从光源视角渲染深度图
- 后处理效果:先渲染到纹理,再做后处理(模糊、Bloom等)
添加HUD相机:
osg::Camera* createHUD()
{
osg::ref_ptr<osg::Camera> camera = new osg::Camera;
// 设置为正交投影
camera->setProjectionMatrix(osg::Matrix::ortho2D(0, 1024, 0, 768));
// 设置为绝对参考系(不跟随主相机移动)
camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF);
// 设置渲染顺序:在主相机之后渲染
camera->setRenderOrder(osg::Camera::POST_RENDER);
// 只清除深度缓冲(不清除颜色,这样可以叠加在主场景上)
camera->setClearMask(GL_DEPTH_BUFFER_BIT);
// 添加HUD内容
camera->addChild(hudContent.get());
// 设置视口(整个屏幕)
camera->setViewport(0, 0, 1024, 768);
return camera.release();
}
// 使用:将HUD相机添加到Viewer中
viewer.addSlave(createHUD());
// 或者作为场景的子节点
root->addChild(createHUD());
7.2 Render to Texture(RTT)
渲染到纹理(Render to Texture,简称RTT)是很多高级渲染效果的基础。它的基本思想是:先将场景渲染到一个纹理中,然后再把这个纹理用到后续的渲染中。
OSG支持多种RTT实现方式:
| 方式 | 说明 | 性能 |
|---|---|---|
FRAME_BUFFER_OBJECT | 使用FBO(帧缓冲对象) | 最好,推荐 |
PIXEL_BUFFER | 使用PBuffer | 较好,兼容性强 |
FRAME_BUFFER | 直接读回帧缓冲 | 最差,不推荐 |
创建RTT相机:
osg::Camera* createRTTCamera(int width, int height, osg::Texture2D* texture)
{
osg::ref_ptr<osg::Camera> camera = new osg::Camera;
// 设置渲染目标为FBO
camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT);
// 设置渲染纹理(颜色缓冲)
camera->attach(osg::Camera::COLOR_BUFFER, texture);
// 设置视口大小
camera->setViewport(0, 0, width, height);
// 设置为预渲染(在主相机之前渲染)
camera->setRenderOrder(osg::Camera::PRE_RENDER);
// 设置场景
camera->addChild(scene.get());
return camera.release();
}
RTT的应用非常广泛,包括:
- 阴影贴图(Shadow Mapping)
- 环境贴图(环境反射、折射)
- 后处理效果(模糊、Bloom、色调映射等)
- 延迟渲染(Deferred Rendering)
- 镜面反射、水面效果
- 等等
7.3 多窗口与多屏显示
OSG支持多窗口和多屏显示。你可以创建多个窗口,每个窗口显示场景的不同视角,或者将一个大场景分散到多个屏幕上(如多屏拼接、沉浸式显示系统)。
多窗口的创建:
osgViewer::CompositeViewer viewer;
// 第一个视图
osg::ref_ptr<osgViewer::View> view1 = new osgViewer::View;
view1->setSceneData(scene.get());
view1->setUpViewInWindow(100, 100, 800, 600);
viewer.addView(view1.get());
// 第二个视图
osg::ref_ptr<osgViewer::View> view2 = new osgViewer::View;
view2->setSceneData(scene.get());
view2->setUpViewInWindow(1000, 100, 800, 600);
viewer.addView(view2.get());
return viewer.run();
多屏拼接:
对于多屏拼接显示(如用多个投影仪组成一个大的显示墙),OSG提供了专门的配置类:
AcrossAllScreens:跨越所有屏幕SingleScreen:单个屏幕SphericalDisplay:球面显示(球幕影院)PanoramicSphericalDisplay:全景球面显示WoWVxDisplay:特定的多屏显示配置
这些配置类可以很方便地设置复杂的多屏显示系统。
八、OpenGL状态管理机制
8.1 State类
osg::State是OSG中管理OpenGL状态的类。它跟踪当前的OpenGL状态,并且只在状态真正变化时才调用OpenGL函数。这就是所谓的"状态缓存"或"懒状态设置"。
class OSG_EXPORT State : public Object
{
public:
State();
// 应用状态属性
void apply(StateAttribute* attribute);
// 应用模式(开/关)
void applyMode(GLenum mode, bool value);
// 应用纹理属性
void applyTextureAttribute(unsigned int unit, StateAttribute* attribute);
// 应用纹理模式
void applyTextureMode(unsigned int unit, GLenum mode, bool value);
// 推送/弹出状态组
void pushStateSet(const StateSet* stateset);
void popStateSet(const StateSet* stateset);
// 绑定顶点数组
void bindVertexArrayObject(GLuint vao);
// ...
};
状态缓存的意义:
为什么需要状态缓存?因为OpenGL的状态切换是有代价的。如果你连续两次设置相同的状态,第二次就是完全浪费的。通过缓存当前状态,OSG可以避免这些冗余的状态设置。
举个例子,如果100个物体都使用同一个纹理,传统方式会调用100次glBindTexture()。而OSG的状态缓存只会在第一个物体时调用一次,后面的99次都会被跳过。
这种优化对于性能提升非常显著,尤其是在状态种类不多但物体很多的场景中。
8.2 状态属性的比较
为了判断一个状态属性是否和当前状态相同,OSG需要比较两个状态属性是否相等。每个StateAttribute子类都需要实现compare()方法:
class OSG_EXPORT StateAttribute : public Object
{
public:
// 返回值:
// 0 表示相同
// 正数表示不同
virtual int compare(const StateAttribute& sa) const = 0;
};
compare()方法返回两个属性之间的"差异程度"。返回0表示完全相同,返回值越大表示差异越大。
这个比较函数不仅用于状态缓存,还用于StateGraph中的状态匹配和排序。
8.3 显示列表(Display List)
显示列表是OpenGL中的一种优化机制——它可以将一系列OpenGL命令编译并缓存起来,以后可以通过调用显示列表来快速重复执行这些命令。
OSG的Drawable支持显示列表。默认情况下,如果OpenGL支持显示列表,OSG会自动使用显示列表来优化静态几何体的渲染。
// 控制是否使用显示列表
drawable->setUseDisplayList(true);
显示列表的优点:
- 可以提高静态几何体的渲染性能
- 将顶点数据缓存在GPU端,减少CPU-GPU数据传输
显示列表的缺点:
- 一旦创建就不能修改(静态的)
- 占用显存
- 在现代GPU上,VBO+VAO的方式通常更灵活
在现代OpenGL中,显示列表已经被弃用(deprecated),取而代之的是顶点缓冲对象(VBO)和顶点数组对象(VAO)。OSG也支持这些现代特性。
8.4 顶点缓冲对象(VBO)
顶点缓冲对象(Vertex Buffer Object,VBO)是OpenGL的一个特性,它允许将顶点数据存储在GPU的显存中,而不是每次渲染都从CPU内存上传。
OSG的Geometry支持VBO。启用VBO后,顶点数据、法线、颜色、纹理坐标、索引等都会被上传到GPU的缓冲对象中,渲染时直接从GPU读取。
// 启用VBO
geometry->setUseVertexBufferObjects(true);
VBO的优点:
- 大幅减少CPU-GPU的数据传输
- 渲染速度更快
- 节省CPU内存带宽
VBO的缺点:
- 占用显存
- 修改数据比直接内存稍微麻烦一点(需要映射缓冲或重新上传)
在现代OpenGL中,VBO已经成为标准做法。OSG默认启用VBO。
顶点数组对象(VAO):
顶点数组对象(Vertex Array Object,VAO)是VBO的"升级版"。VAO可以保存所有顶点数组的配置状态(哪个VBO、什么格式、偏移量等),这样在渲染时只需要绑定VAO就可以了,不需要每次都设置所有的顶点数组指针。
OSG也支持VAO。VAO可以进一步减少绘制调用的开销,特别是在绘制很多小物体的时候。
九、渲染优化技术
9.1 状态排序优化
我们已经讨论了状态排序的基本原理。这里再补充一些实践中的优化技巧:
1. 尽量减少不同状态的数量
场景中的状态种类越少,状态排序的效果越好。尽量复用材质、纹理、着色器等状态。
2. 合理使用状态继承
把公共的状态放到高层节点,这样所有子节点都共享同一份状态,在StateGraph中也会共享同一条路径。
3. 注意透明物体的排序
透明物体需要按深度排序,这会打断状态排序。如果透明物体很多,并且使用不同的状态,性能会下降较多。尽量减少透明物体的数量和状态种类。
9.2 几何体合并
减少绘制调用(Draw Call)的数量是提高渲染性能的重要手段。绘制调用是CPU向GPU发送的"绘制命令",每次调用都有一定的CPU开销。如果场景中有几千甚至上万个小物体,CPU开销可能成为瓶颈。
几何体合并是减少绘制调用的有效方法——将多个小的几何体合并成一个大的几何体,这样只需要一次绘制调用。
OSG的osgUtil::Optimizer提供了几何体合并功能:
osgUtil::Optimizer optimizer;
optimizer.optimize(root.get(),
osgUtil::Optimizer::MERGE_GEOMETRY |
osgUtil::Optimizer::MERGE_GEODES);
MERGE_GEODES会将同一个Group下的多个Geode合并成一个Geode。
MERGE_GEOMETRY会将同一个Geode下的多个Geometry合并成一个Geometry。
合并的注意事项:
几何体合并虽然能减少绘制调用,但也不是越多越好:
-
状态必须相同:只有使用相同状态(材质、纹理、着色器等)的几何体才能合并。如果状态不同,合并后反而会出问题。
-
剔除效率下降:合并后的几何体更大,包围体也更大,这会导致视锥剔除的效率下降。比如,你有100个分散在各处的小物体,每个都可以被独立剔除。但如果合并成一个大物体,只要其中任何一部分可见,整个大物体都要被渲染。
-
LOD困难:合并后的几何体无法单独设置LOD。
-
动态物体无法合并:如果物体是动态的(会移动、变形),合并后就无法独立运动了。
因此,几何体合并需要适度。一般来说,对于静态的、使用相同材质的、空间上靠近的小物体,合并的收益最大。
9.3 纹理图集(Texture Atlas)
纹理图集是另一种减少绘制调用的方法。它的基本思想是:将很多张小纹理合并成一张大纹理(图集),然后使用不同的纹理坐标来引用不同的子纹理。
这样,使用这些纹理的物体就可以共享同一个纹理状态,从而可以合并成一个大的几何体。
OSG的Optimizer提供了纹理图集构建功能:
optimizer.optimize(root.get(), osgUtil::Optimizer::TEXTURE_ATLAS_BUILDER);
纹理图集的优点:
- 减少纹理切换,提高渲染性能
- 使得更多的几何体可以合并
- 可以减少纹理采样的开销
纹理图集的缺点:
- 构建图集需要额外的计算
- 图集大小有限制(不能超过GPU支持的最大纹理尺寸)
- Mipmap边界可能会有渗色问题,需要小心处理
9.4 实例化绘制(Instancing)
实例化绘制(Instanced Rendering)是OpenGL的一个特性,它允许你用一次绘制调用渲染多个相同的几何体(实例),每个实例可以有不同的位置、颜色等属性。
这对于渲染大量重复的物体(如树木、草、粒子等)非常有用。
OSG支持实例化绘制,通过osg::DrawArraysInstanced、osg::DrawElementsInstancedUByte等类实现。
// 创建实例化的图元集
osg::ref_ptr<osg::DrawElementsInstancedUInt> elements =
new osg::DrawElementsInstancedUInt(GL_TRIANGLES);
elements->setNumInstances(1000); // 1000个实例
实例化绘制通常需要配合着色器使用——在着色器中根据实例ID(gl_InstanceID)来获取每个实例的变换等属性。
实例化绘制的性能非常高,特别适合渲染大量相似的物体。在大规模植被、粒子系统等场景中,实例化绘制可以带来数量级的性能提升。
9.5 遮挡剔除
除了视锥剔除,OSG还支持遮挡剔除(Occlusion Culling)——如果一个物体被另一个不透明的物体完全挡住,就不需要渲染它。
OSG的遮挡剔除主要通过OccluderNode实现,我们在前一章已经介绍过。此外,OSG也支持硬件遮挡查询(Occlusion Query),但默认不使用,因为硬件遮挡查询会引入CPU-GPU同步,可能反而降低性能。
在实际项目中,遮挡剔除对于室内场景、城市场景等有大量遮挡物的场景效果非常明显。
9.6 距离剔除与雾效
距离剔除:
对于非常远的物体,可以直接不渲染。OSG的LOD节点可以实现这个效果——只要最高距离的那一层没有子节点,超过距离后物体就会消失。
当然,直接消失可能会比较突兀。可以配合雾效来做过渡——远处的物体逐渐融入雾中,最后消失。
雾效:
雾效不仅可以增加氛围感,还可以隐藏远处的细节,从而可以设置更激进的距离剔除和LOD参数。
osg::ref_ptr<osg::Fog> fog = new osg::Fog;
fog->setMode(osg::Fog::LINEAR); // 线性雾
fog->setStart(100.0f); // 雾开始的距离
fog->setEnd(1000.0f); // 雾最浓的距离
fog->setColor(osg::Vec4(0.7f, 0.7f, 0.8f, 1.0f)); // 雾的颜色
stateset->setAttributeAndModes(fog.get(), osg::StateAttribute::ON);
雾效是一种"以效果换性能"的经典技巧,在很多大规模场景中都有应用。
十、本章总结
在这篇文章中,我们深入探讨了OSG的渲染管线与状态管理系统。让我们回顾一下主要内容:
-
三阶段渲染架构:更新遍历 → 拣选遍历 → 绘制遍历。这种设计使得状态排序、透明排序、多线程等优化成为可能。
-
StateSet状态集:OSG状态管理的核心,支持状态继承、覆盖、保护等机制。将状态与几何分离,提高了灵活性和复用性。
-
StateAttribute状态属性:封装了各种OpenGL渲染状态,包括材质、纹理、着色器、混合、深度等。
-
着色器系统:支持可编程管线,提供了Shader、Program、Uniform等封装,以及ShaderComposer、VirtualProgram等高级特性。
-
CullVisitor拣选遍历:OSG最核心的部分之一,负责视锥剔除、状态合并、变换累积、生成渲染叶子等工作。
-
StateGraph状态图:实现状态排序的核心数据结构,通过将相同状态的物体分组,大幅减少状态切换次数。
-
RenderBin渲染排序:管理不同类型的渲染顺序。不透明物体按状态排序,透明物体按深度排序。
-
osgViewer视图系统:封装了渲染循环、窗口管理、相机操纵等功能。支持多种线程模型。
-
多相机与RTT:支持多相机、多窗口、多屏显示,以及渲染到纹理等高级特性。
-
渲染优化技术:状态排序、几何体合并、纹理图集、实例化绘制、遮挡剔除等。
渲染管线是3D引擎的心脏,理解了OSG的渲染机制,才能写出高效的3D应用。在下一篇文章中,我们将探讨OSG的高级特性与性能优化——包括DatabasePager数据库分页、Optimizer场景优化器、粒子系统、地形渲染、阴影技术等。
本文是「深入理解OpenSceneGraph」系列的第三篇。下一篇我们将聚焦于OSG的高级特性与性能优化,探索如何让你的3D应用跑得更快、更流畅。