
TextureVisitor(纹理访问器)
在 OSG 三维渲染开发中,纹理访问器(TextureVisitor) 是专门用于遍历、提取、修改、管理场景模型纹理贴图的核心工具。
它基于 OSG 最经典的访问者设计模式 实现,能够在不修改场景节点源码、不破坏引擎结构的前提下,自动遍历整棵场景树,批量处理所有模型的纹理、图片、材质状态。
本文结合你提供的完整可运行代码,从原理、继承关系、代码实现、执行流程、应用场景五个维度,彻底讲透纹理访问器。
纹理访问器是什么?解决什么问题?
OSG 场景是一棵树:
Root → Group → Geode → Drawable → StateSet → Texture → Image
纹理贴图(图片)藏在最底层的 StateSet(渲染状态集) 中。
如果没有纹理访问器,你想要:
- 提取模型所有贴图
- 替换所有纹理
- 统一修改纹理参数
- 清理无效纹理
只能手写递归遍历所有节点,代码冗余、容易出错、扩展性极差。
纹理访问器的价值:
- 自动遍历:引擎自动递归整棵场景树
- 非侵入式:不修改 Node/Geode/Texture 源码
- 集中处理:所有纹理逻辑写在一个类里
- 高度复用:一套访问器,支持所有模型
核心原理
纹理访问器的底层原理和顶点访问器完全一致,都是 OSG 访问者模式的标准实现:
1. 两大角色
- 被访问者 :
osg::Node、osg::Geode、osg::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. 纹理统计与检查
- 统计场景贴图数量、尺寸、格式
- 检查超大贴图、异常纹理
总结
- 纹理访问器 = 自动遍历 + 纹理批量操作
- 基类:osg::NodeVisitor
- 核心方法:apply(StateSet) → 提取 Texture2D → 取 Image
- 执行流程:accept → apply → traverse
- 应用:提取、换肤、压缩、清理、统计
