【OSG学习笔记】Day 41: ReadFileCallback 与 WriteFileCallback(自定义文件读取)

ReadFileCallback 与 WriteFileCallback 自定义文件

在 OpenSceneGraph(OSG)开发中,osgDB 模块承担着模型、图片、场景文件的核心读写工作,是连接外部资源与 OSG 场景图的关键桥梁。

但原生的读写逻辑仅能完成基础的文件加载与保存,无法满足开发中的耗时统计、日志打印、权限校验、加密解密、格式预处理等定制化需求。

为此,OSG 提供了回调机制 ------ReadFileCallback(文件读取回调)和 WriteFileCallback(文件写入回调),允许开发者在不修改 OSG 底层源码的前提下,无缝扩展文件读写的前后逻辑,实现灵活的功能增强。

本文将结合完整可运行代码,深度解析两个回调类的继承关系、核心原理、使用方法与实战应用

核心类继承关系(底层架构)

理解 OSG 回调的第一步,是明确其类继承体系,这决定了回调的触发逻辑与重载规则:

1. 完整继承链

复制代码
osg::Referenced(基类:智能指针内存管理)
        ↓
osgDB::Registry::ReadFileCallback (读取回调抽象类)
        ↓(自定义继承)
MyReadFileCallback(开发者实现类)

osg::Referenced(基类:智能指针内存管理)
        ↓
osgDB::Registry::WriteFileCallback(写入回调抽象类)
        ↓(自定义继承)
MyWriteFileCallback(开发者实现类)

2. 关键类作用解析

  1. osg::Referenced

    OSG 所有动态对象的顶层基类 ,提供引用计数智能指针osg::ref_ptr)管理内存,无需手动释放回调对象,避免内存泄漏。

  2. osgDB::Registry::ReadFileCallback

    • 抽象类,定义文件读取的回调接口
    • 包含纯虚函数 readNode(),必须由子类重载实现;
    • 全局唯一,注册后所有 osgDB::readNodeFile() 都会触发该回调。
  3. osgDB::Registry::WriteFileCallback

    • 抽象类,定义文件写入的回调接口
    • 包含纯虚函数 writeNode(),必须由子类重载实现;
    • 全局唯一,注册后所有 osgDB::writeNodeFile() 都会触发该回调。
  4. osgDB::Registry

    OSG 读写插件的全局管理器,负责:

    • 注册/注销读写回调;
    • 调用底层插件完成实际文件读写(readNodeImplementation/writeNodeImplementation);
    • 管理所有支持的文件格式(.osg/.obj/.fbx/.png 等)。

3. 核心虚函数(必须重载)

回调类 核心虚函数 触发时机
ReadFileCallback readNode(文件名, 配置参数) 调用 osgDB::readNodeFile() 读取模型时
WriteFileCallback writeNode(节点对象, 文件名, 配置参数) 调用 osgDB::writeNodeFile() 保存模型时

注意:重载函数中必须调用 OSG 底层的实现方法readNodeImplementation/writeNodeImplementation),否则文件无法真正读写。

核心原理

OSG 采用钩子(Hook)设计模式,回调的执行流程如下:

  1. 开发者自定义类继承回调抽象类,重载核心虚函数;
  2. osgDB::Registry 全局注册自定义回调;
  3. 调用 readNodeFile/writeNodeFile 时,OSG 优先触发回调函数;
  4. 回调中执行自定义逻辑(计时、日志、校验等);
  5. 回调调用 OSG 底层方法完成实际文件读写;
  6. 读写完成后,回调执行后续自定义逻辑,返回结果。

优势:无侵入式扩展,不修改 OSG 源码,不影响原有读写逻辑,可随时启用/关闭回调。

实战代码实现(完整可运行)

结合继承关系与核心原理,我们实现带耗时统计、日志打印的自定义读写回调:

1. 完整代码

cpp 复制代码
#include <osgViewer/Viewer>
#include <osg/Node>
#include <osg/Group>
#include <osgDB/ReadFile>
#include <osgDB/WriteFile>
#include <osgDB/Registry>
#include <osgUtil/Optimizer>
#include <osg/Timer>
#include <iostream>

// -------------------------- 读取回调实现 --------------------------
// 继承:osg::Referenced → osgDB::Registry::ReadFileCallback
class MyReadFileCallback : public osgDB::Registry::ReadFileCallback
{
public:
    // 重载核心虚函数:读取节点
    virtual osgDB::ReaderWriter::ReadResult readNode(
        const std::string& fileName, 
        const osgDB::ReaderWriter::Options* options
    ) override
    {
        // 自定义逻辑1:打印开始日志
        std::cout << "[回调] 开始读取文件:" << fileName << std::endl;
        // 计时开始
        osg::Timer_t start = osg::Timer::instance()->tick();

        // 核心:调用OSG底层实现,完成实际文件读取
        osgDB::ReaderWriter::ReadResult result = 
            osgDB::Registry::instance()->readNodeImplementation(fileName, options);

        // 自定义逻辑2:计算耗时、打印结束日志
        double elapsed = osg::Timer::instance()->delta_s(start, osg::Timer::instance()->tick());
        std::cout << "[回调] 读取完成,耗时:" << elapsed << "s" << std::endl << std::endl;

        return result;
    }
};

// -------------------------- 写入回调实现 --------------------------
// 继承:osg::Referenced → osgDB::Registry::WriteFileCallback
class MyWriteFileCallback : public osgDB::Registry::WriteFileCallback
{
public:
    // 重载核心虚函数:写入节点
    virtual osgDB::ReaderWriter::WriteResult writeNode(
        const osg::Node& obj, 
        const std::string& fileName,
        const osgDB::ReaderWriter::Options* options
    ) override
    {
        // 自定义逻辑1:打印开始日志
        std::cout << "[回调] 开始写入文件:" << fileName << std::endl;
        // 计时开始
        osg::Timer_t start = osg::Timer::instance()->tick();

        // 核心:调用OSG底层实现,完成实际文件写入
        osgDB::ReaderWriter::WriteResult result = 
            osgDB::Registry::instance()->writeNodeImplementation(obj, fileName, options);

        // 自定义逻辑2:计算耗时、打印结束日志
        std::cout << "[回调] 写入完成,耗时:" 
                  << osg::Timer::instance()->delta_s(start, osg::Timer::instance()->tick()) 
                  << "s" << std::endl << std::endl;

        return result;
    }
};

// -------------------------- 主函数 --------------------------
int main()
{
    osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer();
    osg::ref_ptr<osg::Group> root = new osg::Group();

    // 全局注册回调:注册后所有读写操作自动触发
    osgDB::Registry::instance()->setReadFileCallback(new MyReadFileCallback());
    osgDB::Registry::instance()->setWriteFileCallback(new MyWriteFileCallback());

    // 1. 读取模型(触发ReadFileCallback)
    osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("spaceship.osg");
    if (!model.valid()) {
        std::cerr << "模型读取失败!" << std::endl;
        return -1;
    }
    root->addChild(model);

    // 2. 保存模型(触发WriteFileCallback)
    osgDB::writeNodeFile(*root, "output_model.osg");

    // 优化场景并渲染
    osgUtil::Optimizer optimizer;
    optimizer.optimize(root);
    viewer->setSceneData(root);
    viewer->realize();
    return viewer->run();
}

2. 代码核心说明

  1. 继承与重载 :自定义类严格继承 ReadFileCallback/WriteFileCallback,使用 override 关键字确保虚函数重载正确;
  2. 必须调用底层方法readNodeImplementation/writeNodeImplementation 是 OSG 实际读写文件的逻辑,不可省略;
  3. 全局注册 :通过 osgDB::Registry 单例注册回调,一次注册,全局生效;
  4. 内存安全 :回调对象由 osg::Referenced 管理,无需手动 delete

进阶扩展

基于 ReadFileCallbackWriteFileCallback,可实现大量企业级需求:

  1. 权限校验:读取前校验文件权限,拒绝非法文件访问;
  2. 加密解密:读取时解密加密模型,写入时加密保存,保护资源安全;
  3. 格式自动转换:将 .obj/.fbx 自动转为 .osg 缓存,提升加载速度;
  4. 全链路日志:记录所有文件读写的文件名、耗时、结果,用于性能监控;
  5. 异常处理:读写失败时自动重试、上报错误,提升程序稳定性。

总结

  1. 继承关系 :回调类最终继承自 osg::Referenced,依托 osgDB::Registry 实现全局管理;
  2. 核心价值:无侵入式扩展 OSG 读写逻辑,满足定制化开发需求;
  3. 使用关键:继承抽象类 → 重载虚函数 → 调用底层实现 → 全局注册;
  4. 实战价值 :广泛应用于三维仿真、数字孪生、游戏开发等 OSG 项目中,是进阶开发必备技能。
相关推荐
FQNmxDG4S7 小时前
Java多线程编程:Thread与Runnable的并发控制
java·开发语言
Slow菜鸟7 小时前
AI学习篇(五) | awesome-design-md 使用说明
人工智能·学习
ZC跨境爬虫8 小时前
跟着 MDN 学 HTML day_9:(信件语义标记)
前端·css·笔记·ui·html
狐狐生风8 小时前
LangChain 向量存储:Chroma、FAISS
人工智能·python·学习·langchain·faiss·agentai
虹科网络安全8 小时前
艾体宝干货|数据复制详解:类型、原理与适用场景
java·开发语言·数据库
狐狐生风8 小时前
LangChain RAG 基础
人工智能·python·学习·langchain·rag·agentai
axng pmje8 小时前
Java语法进阶
java·开发语言·jvm
rKWP8gKv79 小时前
Java微服务性能监控:Prometheus与Grafana集成方案
java·微服务·prometheus
老前端的功夫9 小时前
【Java从入门到入土】28:Stream API:告别for循环的新时代
java·开发语言·python
qq_435287929 小时前
第9章 夸父逐日与后羿射日:死循环与进程终止?十个太阳同时值班的并行冲突
java·开发语言·git·死循环·进程终止·并行冲突·夸父逐日