【OSG学习笔记】Day 38: TextureVisitor(纹理访问器)

TextureVisitor(纹理访问器)

在 OSG 三维渲染开发中,纹理访问器(TextureVisitor) 是专门用于遍历、提取、修改、管理场景模型纹理贴图的核心工具。

它基于 OSG 最经典的访问者设计模式 实现,能够在不修改场景节点源码、不破坏引擎结构的前提下,自动遍历整棵场景树,批量处理所有模型的纹理、图片、材质状态。

本文结合你提供的完整可运行代码,从原理、继承关系、代码实现、执行流程、应用场景五个维度,彻底讲透纹理访问器。


纹理访问器是什么?解决什么问题?

OSG 场景是一棵树:
Root → Group → Geode → Drawable → StateSet → Texture → Image

纹理贴图(图片)藏在最底层的 StateSet(渲染状态集) 中。

如果没有纹理访问器,你想要:

  • 提取模型所有贴图
  • 替换所有纹理
  • 统一修改纹理参数
  • 清理无效纹理

只能手写递归遍历所有节点,代码冗余、容易出错、扩展性极差。

纹理访问器的价值:

  1. 自动遍历:引擎自动递归整棵场景树
  2. 非侵入式:不修改 Node/Geode/Texture 源码
  3. 集中处理:所有纹理逻辑写在一个类里
  4. 高度复用:一套访问器,支持所有模型

核心原理

纹理访问器的底层原理和顶点访问器完全一致,都是 OSG 访问者模式的标准实现:

1. 两大角色

  • 被访问者osg::Nodeosg::Geodeosg::StateSet
  • 访问者TextureVisitor(你自定义的类)

2. 执行流程(灵魂)

复制代码
node->accept(visitor)  // 节点允许访问
        ↓
visitor.apply(node)    // 访问器处理节点
        ↓
visitor.apply(geode)   // 处理模型
        ↓
visitor.apply(stateSet) // 处理纹理状态(核心)
        ↓
traverse(node)         // 继续遍历子节点

3. 双重分发

运行时自动识别节点类型 ,调用对应的 apply 函数:

  • 遇到 Node → 调用 apply(Node&)
  • 遇到 Geode → 调用 apply(Geode&)
  • 遇到 StateSet → 调用 apply(StateSet&)

这是 OSG 能实现自动类型匹配、自动遍历的核心秘密。


类继承关系(必须掌握)

1. 访问器继承链

复制代码
osg::Referenced
       ↓
osg::NodeVisitor  <-- 所有访问器的基类
       ↓
TextureVisitor    <-- 你实现的纹理访问器

2. 节点与纹理继承链

复制代码
osg::Node
   ↓
osg::Geode        // 模型节点
   ↓
osg::Drawable     // 几何体
   ↓
osg::StateSet     // 渲染状态(存放纹理)
   ↓
osg::Texture2D    // 2D 纹理
   ↓
osg::Image        // 真正的图片数据(像素)

3. 关键类作用

  • NodeVisitor:提供遍历框架
  • StateSet:管理纹理、材质、着色器
  • Texture2D:2D 纹理对象
  • Image:内存中的图像(可保存为文件)

完整代码实现深度解析

下面结合可直接编译运行的代码,逐模块讲解纹理访问器实现。

c 复制代码
/********************************************************
 * Write by FlySky
 * zzuxp@163.com  http://www.OsgChina.org
 ********************************************************/
#include <osgViewer/Viewer>
#include <osg/Node>
#include <osg/Geode>
#include <osg/Group>
#include <osg/NodeVisitor>
#include <osgDB/ReadFile>
#include <osgDB/WriteFile>
#include <osgUtil/Optimizer>
#include <iostream>
#include <string>
#include <map>
#include <cstdio>

// 节点纹理访问器
class TextureVisitor : public osg::NodeVisitor
{
public:
    TextureVisitor()
        : osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN)
    {
    }

    // 处理 Node
    virtual void apply(osg::Node& node)
    {
        if (node.getStateSet())
        {
            // 修复:传指针,不是引用!
            apply(node.getStateSet());
        }
        traverse(node);
    }

    // 处理 Geode
    virtual void apply(osg::Geode& geode)
    {
        if (geode.getStateSet())
        {
            // 修复:传指针
            apply(geode.getStateSet());
        }

        unsigned int cnt = geode.getNumDrawables();
        for (unsigned int i = 0; i < cnt; i++)
        {
            osg::Drawable* drawable = geode.getDrawable(i);
            if (drawable && drawable->getStateSet())
            {
                // 修复:传指针!!!(这行就是报错位置)
                apply(drawable->getStateSet());
            }
        }
        traverse(geode);
    }

    // 核心:处理 StateSet(指针形式)
    void apply(osg::StateSet* state)
    {
        if (!state) return;

        osg::StateSet::TextureAttributeList& texAttribList = state->getTextureAttributeList();
        
        for (unsigned int i = 0; i < texAttribList.size(); i++)
        {
            osg::Texture* tex = dynamic_cast<osg::Texture*>(state->getTextureAttribute(i, osg::StateAttribute::TEXTURE));
            if (tex)
            {
                osg::Texture2D* tex2D = dynamic_cast<osg::Texture2D*>(tex);
                if (tex2D)
                {
                    osg::Image* image = tex2D->getImage();
                    if (image)
                    {
                        std::string name = image->getFileName();
                        if (name.empty()) 
                        {
                            char uuid[36];
                            sprintf(uuid, "Unknown_Texture_%p", image);
                            name = std::string(uuid);
                        }
                        _imageList.insert(std::make_pair(name, image));
                    }
                }
            }
        }
    }

    std::map<std::string, osg::ref_ptr<osg::Image>>& getImages()
    {
        return _imageList;
    }

protected:
    std::map<std::string, osg::ref_ptr<osg::Image>> _imageList;
};

int main()
{
    osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer();
    osg::ref_ptr<osg::Group> root = new osg::Group();

    osg::ref_ptr<osg::Node> node = osgDB::readNodeFile("cow.osg");
    if (!node)
    {
        std::cerr << "错误:无法加载模型文件 cow.osg" << std::endl;
        return -1;
    }

    TextureVisitor textureTV;
    node->accept(textureTV);

    auto& imageList = textureTV.getImages();
    auto iter = imageList.begin();
    unsigned int cnt = 0;
    char buffer[2000];

    std::cout << "检测到 " << imageList.size() << " 张贴图。正在导出..." << std::endl;

    for (; iter != imageList.end(); ++iter)
    {
        sprintf(buffer, "Texture_%d.jpg", ++cnt);
        bool result = osgDB::writeImageFile(*(iter->second.get()), buffer);
        
        if (result)
            std::cout << "成功导出: " << buffer << std::endl;
        else
            std::cerr << "导出失败: " << buffer << std::endl;
    }

    root->addChild(node.get());
    osgUtil::Optimizer optimizer;
    optimizer.optimize(root.get());

    viewer->setSceneData(root.get());
    viewer->realize();
    return viewer->run();
}

1. 类定义与构造

cpp 复制代码
class TextureVisitor : public osg::NodeVisitor
{
public:
    // 遍历所有子节点
    TextureVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) {}
  • 必须继承 osg::NodeVisitor
  • TRAVERSE_ALL_CHILDREN:自动遍历所有子节点

2. 处理普通节点

cpp 复制代码
void apply(osg::Node& node) override
{
    if (node.getStateSet())
        apply(node.getStateSet());
    traverse(node); // 继续遍历
}

3. 处理模型节点(Geode)

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

    // 遍历所有几何体
    for (unsigned i=0; i<geode.getNumDrawables(); i++) {
        auto d = geode.getDrawable(i);
        if (d && d->getStateSet())
            apply(d->getStateSet());
    }
    traverse(geode);
}

4. 核心:提取纹理与图片(最关键)

cpp 复制代码
void apply(osg::StateSet* state)
{
    if (!state) return;

    // 遍历所有纹理单元
    auto& texList = state->getTextureAttributeList();
    for (unsigned i=0; i<texList.size(); i++)
    {
        // 取出纹理
        osg::Texture* tex = dynamic_cast<osg::Texture*>(
            state->getTextureAttribute(i, osg::StateAttribute::TEXTURE)
        );

        // 转为 2D 纹理
        osg::Texture2D* tex2d = dynamic_cast<osg::Texture2D*>(tex);
        if (tex2d && tex2d->getImage())
        {
            // 保存图片到 map
            _imageList[tex2d->getImage()->getFileName()] = tex2d->getImage();
        }
    }
}

5. 主函数使用

cpp 复制代码
TextureVisitor tv;
node->accept(tv); // 启动访问

auto& images = tv.getImages();
for (auto& pair : images) {
    osgDB::writeImageFile(*pair.second, "Texture.jpg");
}

纹理访问器执行流程(极简总结)

复制代码
1. 调用 node->accept(tv)
2. 访问器进入节点
3. 自动调用 apply(Node) / apply(Geode)
4. 取出 StateSet
5. 遍历纹理 → 转 Texture2D → 取 Image
6. 存入 map
7. traverse 继续遍历子节点
8. 遍历完成 → 得到所有纹理图片

纹理访问器的应用场景(超级实用)

纹理访问器是OSG 工程化开发必备工具,覆盖 90% 纹理相关需求:

1. 批量提取模型贴图(最常用)

  • 导出模型所有纹理到本地
  • 资源解包、查看模型材质
  • 你现在运行的代码就是这个功能

2. 批量替换纹理(一键换肤)

  • 场景中所有模型统一换贴图
  • 模拟昼夜、季节、皮肤切换

3. 纹理压缩与优化

  • 自动压缩贴图大小
  • 降低内存占用
  • 提升渲染性能

4. 清理无效纹理

  • 删除没有使用的贴图
  • 修复模型加载异常

5. 批量修改纹理参数

  • 统一开启/关闭纹理过滤
  • 统一设置 Wrap 模式
  • 统一修改纹理单元

6. 纹理统计与检查

  • 统计场景贴图数量、尺寸、格式
  • 检查超大贴图、异常纹理

总结

  1. 纹理访问器 = 自动遍历 + 纹理批量操作
  2. 基类:osg::NodeVisitor
  3. 核心方法:apply(StateSet) → 提取 Texture2D → 取 Image
  4. 执行流程:accept → apply → traverse
  5. 应用:提取、换肤、压缩、清理、统计

相关推荐
JJay.2 小时前
Kotlin 高阶函数学习指南
android·开发语言·kotlin
jinanwuhuaguo2 小时前
截止到4月8日,OpenClaw 2026年4月更新深度解读剖析:从“能力回归”到“信任内建”的范式跃迁
android·开发语言·人工智能·深度学习·kotlin
杨云龙UP2 小时前
从0到1快速学会Linux操作系统(基础),这一篇就够了!
linux·运维·服务器·学习·ubuntu·centos·ssh
JJay.3 小时前
Android Kotlin 协程使用指南
android·开发语言·kotlin
头疼的程序员3 小时前
计算机网络:自顶向下方法(第七版)第八章 学习分享(三)
网络·学习·计算机网络
BLUcoding3 小时前
Android 布局介绍
android
summerkissyou19873 小时前
android-蓝牙-状态和协议值总结及监听例子
android·蓝牙
徒 花3 小时前
数据库知识复习05
android·数据库
_李小白3 小时前
【OSG学习笔记】Day 37: NodeVisitor(顶点访问器)
笔记·学习