深入理解OpenSceneGraph(三):渲染管线与状态管理

1 阅读40分钟

深入理解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最核心、最复杂的阶段。它的主要任务是:

  1. 视锥剔除:判断哪些节点在视锥体内,哪些在外面
  2. 状态收集:从根节点到叶子节点,收集并合并所有的渲染状态
  3. 变换累积:累积每个节点的变换矩阵,得到世界坐标
  4. 生成渲染叶子:将可见的Drawable包装成RenderLeaf
  5. 渲染排序:按照状态和深度对RenderLeaf进行排序
  6. 构建渲染图:最终生成一棵StateGraph和多个RenderBin

拣选遍历的输出是一个"渲染图"(Render Graph),它已经不是场景图了,而是按照渲染效率优化过的数据结构。

第三阶段:绘制遍历(Draw)

绘制遍历是最简单也最"重"的阶段。它遍历拣选阶段生成的渲染图,依次调用OpenGL API执行实际的绘制命令。这个阶段的工作主要是:

  1. 按照状态图设置OpenGL状态
  2. 按照RenderBin的顺序绘制
  3. 执行每个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在拣选遍历中从父节点进入子节点时,会进行状态合并。对于每个状态属性:

  1. 如果子节点的属性有PROTECTED标志,则使用子节点的属性
  2. 否则,如果父节点的属性有OVERRIDE标志,则使用父节点的属性
  3. 否则,如果子节点有该属性,则使用子节点的属性
  4. 否则,使用父节点的属性

这个规则确保了状态的正确合并,同时提供了足够的灵活性。

2.3 状态属性(StateAttribute)

osg::StateAttribute是所有状态属性的基类。OSG内置了大量的状态属性子类,覆盖了OpenGL渲染状态的方方面面。

主要的状态属性包括:

属性类别代表类功能
材质Material环境光、漫反射、高光、发射光颜色,光泽度
光照模型LightModel全局光照模型设置
纹理Texture2D, TextureCubeMap等纹理图像与纹理参数
着色器Program, Shader, UniformGLSL着色器程序
混合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中,透明物体的渲染需要满足两个条件:

  1. 要启用混合(GL_BLEND)
  2. 透明物体必须从后往前绘制(先画远的,再画近的)

这是因为透明混合的公式是:结果 = 源颜色 * 源alpha + 目标颜色 * (1 - 源alpha)。如果先画近的再画远的,远处的物体就无法透过近处的物体"看到",因为近处的物体已经把深度缓冲写死了。

OSG的RenderBin机制:

OSG将渲染分为多个"Bin",每个Bin有一个编号和排序方式:

Bin名称编号排序方式用途
StateGraphBin-2按状态排序不透明物体(默认)
DepthSortedBin10按深度从远到近排序透明物体
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_CONTROLtessellation控制着色器
TESS_EVALUATIONtessellation评估着色器
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的基本思想是:

  1. 定义一组"着色器组件",每个组件实现一个功能(如Phong光照、法线贴图、骨骼动画等)
  2. 定义组件之间的依赖关系
  3. 在运行时,根据物体的状态自动选择需要的组件,组合成完整的着色器

这种方式类似于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会执行以下步骤:

  1. 获取节点的局部包围球
  2. 将包围球变换到视图空间(或者将裁剪平面变换到局部空间)
  3. 检测包围球与视锥体六个平面的位置关系
  4. 根据检测结果决定:
    • 完全在外面:跳过该节点及其所有子节点
    • 完全在里面:不需要再检查子节点(所有子节点肯定也在里面),直接全部加入渲染队列
    • 相交:需要继续检查子节点

这个过程是递归的,形成了一个"剔除树"。对于平衡的场景图,剔除的时间复杂度是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选择的步骤:

  1. 计算LOD节点中心到相机的距离(或屏幕像素大小)
  2. 根据距离查找对应的子节点索引
  3. 只遍历选中的那个子节点(其他子节点被跳过)

这个过程是在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会:

  1. 将该物体的完整状态集"分解"成一系列的状态变更
  2. 从StateGraph的根节点开始,沿着匹配的路径往下走
  3. 如果某个状态变更是当前节点没有的,就创建一个新的子节点
  4. 最后把RenderLeaf放到对应的叶子节点

StateGraph的优势:

  1. 状态共享:相同的状态前缀只需要设置一次。渲染时,沿着StateGraph的路径走,每进入一个子节点就应用它的状态变更,不需要每次都重新设置完整的状态。

  2. 自动排序:由于相同状态的物体被放在同一个StateGraph节点下,渲染时自然就是按状态分组的。

  3. 高效合并:场景图中的状态继承关系可以很自然地映射到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:多视图查看器,可以有多个View
  • View:视图,对应一个相机和一个场景
  • 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驾驶模式,模拟汽车驾驶
UFOManipulatorUFO模式,平滑的飞行控制
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框架中。基本思路是:

  1. 创建一个GUI窗口/控件
  2. 获取它的窗口句柄(HWND、Window ID等)
  3. 创建OSG的GraphicsWindow,指定使用这个窗口句柄
  4. 将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。

合并的注意事项:

几何体合并虽然能减少绘制调用,但也不是越多越好:

  1. 状态必须相同:只有使用相同状态(材质、纹理、着色器等)的几何体才能合并。如果状态不同,合并后反而会出问题。

  2. 剔除效率下降:合并后的几何体更大,包围体也更大,这会导致视锥剔除的效率下降。比如,你有100个分散在各处的小物体,每个都可以被独立剔除。但如果合并成一个大物体,只要其中任何一部分可见,整个大物体都要被渲染。

  3. LOD困难:合并后的几何体无法单独设置LOD。

  4. 动态物体无法合并:如果物体是动态的(会移动、变形),合并后就无法独立运动了。

因此,几何体合并需要适度。一般来说,对于静态的、使用相同材质的、空间上靠近的小物体,合并的收益最大。

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的渲染管线与状态管理系统。让我们回顾一下主要内容:

  1. 三阶段渲染架构:更新遍历 → 拣选遍历 → 绘制遍历。这种设计使得状态排序、透明排序、多线程等优化成为可能。

  2. StateSet状态集:OSG状态管理的核心,支持状态继承、覆盖、保护等机制。将状态与几何分离,提高了灵活性和复用性。

  3. StateAttribute状态属性:封装了各种OpenGL渲染状态,包括材质、纹理、着色器、混合、深度等。

  4. 着色器系统:支持可编程管线,提供了Shader、Program、Uniform等封装,以及ShaderComposer、VirtualProgram等高级特性。

  5. CullVisitor拣选遍历:OSG最核心的部分之一,负责视锥剔除、状态合并、变换累积、生成渲染叶子等工作。

  6. StateGraph状态图:实现状态排序的核心数据结构,通过将相同状态的物体分组,大幅减少状态切换次数。

  7. RenderBin渲染排序:管理不同类型的渲染顺序。不透明物体按状态排序,透明物体按深度排序。

  8. osgViewer视图系统:封装了渲染循环、窗口管理、相机操纵等功能。支持多种线程模型。

  9. 多相机与RTT:支持多相机、多窗口、多屏显示,以及渲染到纹理等高级特性。

  10. 渲染优化技术:状态排序、几何体合并、纹理图集、实例化绘制、遮挡剔除等。

渲染管线是3D引擎的心脏,理解了OSG的渲染机制,才能写出高效的3D应用。在下一篇文章中,我们将探讨OSG的高级特性与性能优化——包括DatabasePager数据库分页、Optimizer场景优化器、粒子系统、地形渲染、阴影技术等。


本文是「深入理解OpenSceneGraph」系列的第三篇。下一篇我们将聚焦于OSG的高级特性与性能优化,探索如何让你的3D应用跑得更快、更流畅。