【OSG学习笔记】Day 37: NodeVisitor(顶点访问器)

NodeVisitor(顶点访问器)

在 OSG(OpenSceneGraph)三维渲染引擎中,顶点访问器(Vertex Visitor) 是操作场景几何数据最核心、最常用的工具。

它基于访问者设计模式 实现,完美实现了场景结构与操作逻辑的解耦,让开发者无需修改引擎源码,就能遍历、提取、修改场景中所有模型的顶点数据。

本文将从设计思想、继承关系、核心API、实战代码、使用场景五个维度,带你彻底掌握 OSG 顶点访问器。


什么是顶点访问器?

顶点访问器是专门用于遍历、处理场景中几何顶点数据的访问器。

OSG 场景是树形结构:
根节点 -> 组节点(Group) -> 几何节点(Geode) -> 几何体(Geometry) -> 顶点数组

顶点数据存储在最底层的 Geometry 中,顶点访问器的使命就是自动遍历整棵树,找到所有顶点并对其操作

核心设计思想

  1. 访问者模式:将"遍历逻辑"与"业务逻辑"分离
  2. 双重分发:运行时动态匹配节点类型,自动调用对应处理函数
  3. 非侵入式:不修改 Node、Geode、Geometry 源码
  4. 自动递归:无需手写递归遍历,引擎自动完成全场景遍历

类继承关系

理解继承关系,才能真正掌握访问器的运行机制。

1. 顶层基类:osg::NodeVisitor

所有访问器的祖宗类,提供遍历框架。

复制代码
osg::Referenced
       ↓
osg::NodeVisitor  <-- 所有访问器必须继承它

2. 顶点访问器继承链

你自定义的顶点访问器继承结构:

复制代码
osg::NodeVisitor
       ↓
你的顶点访问器(VertexVisitor)

3. 被访问节点继承链(顶点数据所在)

复制代码
osg::Node
   ↓
osg::Geode      <-- 几何节点,存放模型
   ↓
osg::Geometry   <-- 真正存储顶点、法线、纹理坐标

4. 顶点数据继承链

复制代码
osg::Array
   ↓
osg::Vec3Array  <-- 存储三维顶点坐标

四大核心 API

这是顶点访问器能工作的四大支柱

1. accept() ------ 入口函数(节点提供)

  • 位置osg::Node

  • 作用:启动访问,允许访问器进入节点

  • 代码

    cpp 复制代码
    node->accept(visitor);

2. apply() ------ 业务逻辑(访问器重写)

  • 位置osg::NodeVisitor

  • 作用 :处理具体节点,顶点访问器必须重写 apply(Geode&)

  • 代码

    cpp 复制代码
    void apply(osg::Geode& geode) override;

3. traverse() ------ 递归遍历

  • 作用 :继续遍历子节点,必须调用,否则遍历中断

  • 代码

    cpp 复制代码
    traverse(geode);

4. validNodeMask() ------ 节点过滤

  • 作用:跳过不需要的节点,提升性能

完整实战代码

这是工业级、可直接编译运行的顶点提取访问器,逐行解析。

完整代码

cpp 复制代码
#include <osgViewer/Viewer>
#include <osg/Node>
#include <osg/Geode>
#include <osg/Geometry>
#include <osgDB/readFile>
#include <fstream>
#include <iostream>

// ==============================================
// 顶点访问器:继承自 osg::NodeVisitor
// ==============================================
class VertexExtractor : public osg::NodeVisitor
{
public:
    // 存储提取的所有顶点
    osg::ref_ptr<osg::Vec3Array> vertices;

    // 构造:设置遍历所有子节点
    VertexExtractor()
        : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN)
    {
        vertices = new osg::Vec3Array;
    }

    // ==========================================
    // 核心:重写 apply,只处理 Geode 模型节点
    // ==========================================
    void apply(osg::Geode& geode) override
    {
        // 遍历 Geode 下的所有几何体
        for (unsigned i = 0; i < geode.getNumDrawables(); ++i)
        {
            // 转为 Geometry(只有它有顶点)
            osg::Geometry* geom = dynamic_cast<osg::Geometry*>(geode.getDrawable(i));
            if (!geom) continue;

            // 获取顶点数组
            osg::Vec3Array* verts = dynamic_cast<osg::Vec3Array*>(geom->getVertexArray());
            if (!verts) continue;

            // 把顶点追加到结果中
            vertices->insert(vertices->end(), verts->begin(), verts->end());
        }

        // 必须遍历子节点
        traverse(geode);
    }
};

// ==============================================
// 主函数:加载模型 + 使用访问器
// ==============================================
int main()
{
    // 1. 加载模型
    osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("glider.osg");

    // 2. 创建顶点访问器
    VertexExtractor extractor;

    // 3. 启动访问(核心:accept)
    model->accept(extractor);

    // 4. 输出顶点数量
    std::cout << "提取顶点数:" << extractor.vertices->size() << std::endl;

    // 5. 保存到文件
    std::ofstream out("vertices.txt");
    for (auto& v : *extractor.vertices)
        out << v.x() << " " << v.y() << " " << v.z() << "\n";
    out.close();

    // 显示
    osgViewer::Viewer viewer;
    viewer.setSceneData(model);
    return viewer.run();
}

代码执行流程

复制代码
model->accept(extractor)
       ↓
extractor.apply(geode)  处理模型
       ↓
提取 Geometry 顶点
       ↓
traverse(geode)  继续遍历子节点
       ↓
遍历完成 → 得到所有顶点

应用场景

顶点访问器是 OSG 开发必备工具,覆盖 90% 几何操作:

1. 数据导出与格式转换

  • 导出顶点、法线、UV 坐标
  • 转成 JSON/CSV/自定义格式
  • 模型轻量化、数据清洗

2. 模型修改与实时形变

  • 水面波浪动画
  • 模型破碎、拉伸、扭曲
  • 顶点位移、缩放

3. 碰撞检测与拾取

  • 鼠标点击获取顶点
  • 检测交点所在三角面
  • 物理仿真碰撞计算

4. 网格优化与处理

  • 生成法线
  • 简化模型、减少三角面
  • 计算包围盒、中心点

5. 分析与统计

  • 统计三角面数量
  • 检查模型错误
  • 计算模型体积、面积

总结

  1. 解耦:场景结构 ↔ 操作逻辑
  2. 自动遍历:不用手写递归
  3. 非侵入:不修改引擎源码
  4. 灵活扩展:一个访问器实现一种功能
  5. 工业必备:仿真、车载、数字孪生必用
相关推荐
程序员雷欧2 小时前
大模型应用开发学习第八天
大数据·人工智能·学习
SccTsAxR3 小时前
算法基石:手撕离散化、递归与分治
c++·经验分享·笔记·算法
晓晓hh3 小时前
JavaSE学习——set集合和Map映射
学习
西梅汁3 小时前
C++ 观察者模式
笔记
لا معنى له4 小时前
Var-JEPA:联合嵌入预测架构的变分形式 —— 连接预测式与生成式自监督学习 ----论文翻译
人工智能·笔记·学习·语言模型
世人万千丶4 小时前
Flutter 框架跨平台鸿蒙开发 - 宠物语言翻译器应用
学习·flutter·华为·开源·harmonyos·鸿蒙
AI成长日志4 小时前
【笔面试算法学习专栏】哈希表基础:两数之和与字母异位词分组
学习·算法·面试
"菠萝"4 小时前
C#知识学习-021(文字关键字)
开发语言·学习·c#
chase。4 小时前
【学习笔记】让机器人“边想边动”——实时动作分块流策略的执行方法
笔记·学习·机器人