文章目录
- 前言
- 一、OpenSceneGraph库
- 1. OSG源码
- 2. 编译教程
- 2. Windows编译完成版
 
- 二、osgconv格式转换工具
- 1. osgconv官方说明文档
- 2. osgconv工具调用
 
- 三、基于C++格式转换,简化OBJ,输出纹理到指定目录
- 1. 项目环境
- 2. 完整代码
- 3. 可执行文件
 
前言
本文基于OpenSceneGraph,使用C++实现其他三维模型格式到OBJ的转换,并简化、输出纹理图片到指定目录。
一、OpenSceneGraph库
1. OSG源码
(1)中文官网:osgChina.org
 (2)英文官网:OpenSceneGraph.com
 (3)下载源码和第三方库:OpenSceneGraph源码和第三方库
2. 编译教程
(1)Github编译教程:OpenSceneGraph git repository
 (2)其他编译参考:
| Windows | Linux | 
|---|---|
| Windows10编译安装OpenSceneGraph(OSG)教程 | Ubuntu环境OSG的编译、安装与使用 | 
| windows平台下用CMake编译osg | linux环境编译OpenSceneGraph和osgEarth | 
| OSG环境部署 OSG3.6.5+vs2017+win10_x64 | 非免费 | 
2. Windows编译完成版
(1)OpenScenGraph3.6.5_Windows.zip
 
 (2)说明:上面的版本已经能够满足很多需求,但是FBX插件存在问题,可以下载FBX SDK编译(Autodesk FBX SDK 下载)(Ubuntu编译FBX问题参考)。
二、osgconv格式转换工具
提示:osgconv工具在bin目录中
1. osgconv官方说明文档
osgconv 主要的功能是用来将3D模型进行格式转换和进行一些诸如纹理压缩类的操作的:osgconv用户使用指南。
2. osgconv工具调用
以下以Windows为例,将ive转换为obj,Linux需要把 osgconv.exe 命令换成./osgconv
- 不转换出纹理
osgconv.exe cow.ive test\\cow.obj
说明:输出obj的同时也会输出同名的mtl文件,都在test目录下.
 2. 转换出纹理
osgconv.exe -O OutputTextureFiles cow.ive cow.obj
说明:输出的纹理图片会存储在images文件夹下,而images与输出的obj文件位于同一目录;但这种方法存在缺陷,如果更改obj的输出目录,如test\\cow.obj,正常情况下images也会在test目录中,但结果并不会,所以不推荐.
 3. 转换出压缩纹理
osgconv.exe --compressed-dxt5 cow.ive test\\cow.obj
说明:输出的纹理图片与obj文件位于同一目录,且格式全部为dds,不会像上一个方法不能导出纹理到OBJ输出目录.
 4. 转为OBJ的问题
 其他格式转换为OBJ时会默认将模型绕X轴旋转90度,因此上面命令最好添加另一个参数,以转换出压缩纹理为例:
osgconv.exe --compressed-dxt5 -o -90-1,0,0 cow.ive test\\cow.obj
说明:-90-1,0,0代表绕着X轴旋转-90度,详情参看官方文档.
三、基于C++格式转换,简化OBJ,输出纹理到指定目录
提示:为了解决以上问题,可以调用OSG的dll进行修改,以下内容以将3ds格式数据转换为OBJ,并进行简化,输出纹理到指定目录
1. 项目环境
- Visual Studio 2022.
- OpenSceneGraph3.6.5 Windows编译完成版
- 项目配置参考:osg开发配置与第一个osg程序、OSG环境部署(VS2017+WIN10+OSG3.6.5)
2. 完整代码
// 基于OpenSceneGraph开源库,将其他模型转换为OBJ并输出纹理图片到指定路径,包括简化OBJ
// Operating System: Windows 10
#include <Windows.h>
#include <iostream>
#include <string>
#include <map>
#include <fstream>
#include <direct.h> 
#include <cstdlib>
#include <osg/Node>
#include <osg/Geode>
#include <osg/Group>
#include <osg/PositionAttitudeTransform>
#include <osg/MatrixTransform>
#include <osgDB/ReadFile>
#include <osgDB/WriteFile>
#include <osgGA/StateSetManipulator>
#include <osgUtil/Optimizer>
#include <osgUtil/Simplifier>
#include<osg/ComputeBoundsVisitor>
using namespace std;
void splitFileNameAndExtension(const string& fullPath, string& fileName, string& fileExtension); // 从路径中获得文件名和后缀名
void processMTLFile(const string& mtlFile); // 处理MTL文件中的纹理图片路径
void saveTextureFile(string directory, string imageName, osg::Image* image); // 保存纹理图片到指定目录
bool checkAndCreateDirectory(const std::string& directoryPath); // 创建目录
void convertToObj(std::string inputPath, std::string outputDir, float compressLevel, int num); // 转换为OBJ并输出
osg::ref_ptr<osg::Node> moveNodeToOrigin(osg::Node* node); // 将旋转后的OBJ移动回原点(0,0,0)
osg::Node* readModel(string inputFilePath); // 读取三维模型
osg::Node* simplityObj(osg::Node* node, float compressLevel); // 简化OBJ文件
// 纹理访问器类,用于遍历节点并收集纹理信息
class TextureVisitor :public osg::NodeVisitor
{
public:
	TextureVisitor() :osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ACTIVE_CHILDREN)
	{
	}
	// 应用于普通节点
	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++)
		{
			apply(geode.getDrawable(i)->getStateSet());
		}
		traverse(geode);
	}
	// 应用于状态集合(StateSet)
	void apply(osg::StateSet* state)
	{
		osg::StateSet::TextureAttributeList& texAttribList = state->getTextureAttributeList();
		for (unsigned int i = 0; i < texAttribList.size(); i++)
		{
			osg::Texture2D* tex2D = NULL;
			if (tex2D = dynamic_cast<osg::Texture2D*>(state->getTextureAttribute(i, osg::StateAttribute::TEXTURE)))
			{
				if (tex2D->getImage())
				{
					_imageList.insert(std::make_pair(tex2D->getImage()->getFileName(), tex2D->getImage()));
				}
			}
		}
	}
	// 获取收集到的纹理图像列表
	std::map<std::string, osg::Image*>& getImages(void)
	{
		return _imageList;
	}
protected:
	std::map<std::string, osg::Image*> _imageList; // 存储纹理图像的映射
};
int main()
{
	string inputPath = "your_source_model_path";
	string outputDir = "output_directory";
	
	// 例子
	// string inputPath = "D:\\data\\3DS\\test.3ds";
	// string outputDir = "D:\\data\\3ds\\3ds2Obj";
	// 本程序将OBJ简化为三个等级,没有添加LOD参数
	// 高分辨率(未简化)
	string originPath = outputDir + "\\" + to_string(0);
	convertToObj(inputPath,originPath,1.0, 0);
	// 中等分辨率
	string mediumPath = outputDir + "\\" + to_string(1);
	convertToObj(inputPath, mediumPath, 0.7, 1);
	
	// 低分辨率
	string lowPath = outputDir + "\\" + to_string(2);
	convertToObj(inputPath, lowPath, 0.4, 2);
	return 0;
}
// 将其他模型转换为OBJ
void convertToObj(std::string inputPath, std::string outputDir, float simplifyLevel, int level)
{
	// 创建一个根节点
	osg::ref_ptr<osg::Group> root = new osg::Group();
	// 读取模型节点
	osg::ref_ptr<osg::Node> node1 = readModel(inputPath);
	// 对模型进行深拷贝并简化
	osg::ref_ptr<osg::Node> node2 = simplityObj(node1, simplifyLevel);
	osg::ref_ptr<osg::Node> nodeAtOrigin = moveNodeToOrigin(node2);
	TextureVisitor textureTV;
	node1->accept(textureTV);
	std::map<std::string, osg::Image*> imageList = textureTV.getImages();
	std::map<std::string, osg::Image*>::iterator iter = imageList.begin();
	// 创建输出目录
	if (checkAndCreateDirectory(outputDir)) {
		std::cout << "Directory created successfully: " << outputDir << std::endl;
	}
	else {
		std::cerr << "Failed to create directory: " << outputDir << std::endl;
	}
	for (iter = imageList.begin(); iter != imageList.end(); ++iter)
	{// 保存纹理
		std::string imagePath = iter->first;
		osg::Image* image = iter->second;
		std::string imageName;
		std::string imageExtension;
		// 从完整路径中提取文件名和后缀名
		splitFileNameAndExtension(imagePath, imageName, imageExtension);
		 输出纹理名称/路径
		// cout << "Image Name: " << imageName << endl;
		// 确保图像不为空,保存纹理图片
		if (image)
		{
			saveTextureFile(outputDir, imageName, image);
		}
	}
	// 从根节点中移除原始模型
	root->removeChild(node1);
	// osg::ref_ptr<osgDB::Options> options = new osgDB::Options("noRotation");
	// 创建一个Geode
	osg::ref_ptr<osg::Geode> geode = new osg::Geode();
	// 保存简化后的模型到文件
	std::string fileName;
	std::string outExtension;
	splitFileNameAndExtension(inputPath, fileName, outExtension);
	// 输出OBJ到指定层级的目录,并处理MTL文件中的纹理路径
	osgDB::writeNodeFile(*nodeAtOrigin, outputDir + "\\" + to_string(level) + ".obj");
	processMTLFile(outputDir + "\\" + to_string(level) + ".mtl");
}
// 从完整路径中提取文件名和后缀名
void splitFileNameAndExtension(const string& fullPath, string& fileName, string& fileExtension)
{
	size_t lastSlash = fullPath.find_last_of("\\/");
	size_t lastDot = fullPath.find_last_of('.');
	if (lastSlash != string::npos && lastDot != string::npos && lastDot > lastSlash)
	{
		fileName = fullPath.substr(lastSlash + 1, lastDot - lastSlash - 1);
		fileExtension = fullPath.substr(lastDot + 1);
	}
	else if (lastDot != string::npos && (lastSlash == string::npos || lastDot > lastSlash))
	{
		// 如果没有目录分隔符但有点,将点之前的部分作为文件名,点之后的部分作为后缀
		fileName = fullPath.substr(0, lastDot);
		fileExtension = fullPath.substr(lastDot + 1);
	}
	else
	{
		// 如果无法找到合适的分隔符和点,将整个字符串作为文件名,后缀为空
		fileName = fullPath;
		fileExtension = "";
	}
}
// 处理MTL文件,将map_Kd的纹理路径修改为图片名称+png
void processMTLFile(const string& mtlFile) {
	ifstream inputFile(mtlFile);
	if (!inputFile.is_open()) {
		cerr << "Failed to open MTL file: " << mtlFile << endl;
		return;
	}
	// 创建一个临时文件用于保存修改后的内容
	string tempFileName = mtlFile + ".temp";
	ofstream tempFile(tempFileName);
	if (!tempFile.is_open()) {
		cerr << "Failed to create temporary file: " << tempFileName << endl;
		inputFile.close();
		return;
	}
	string line;
	while (getline(inputFile, line)) {
		if (line.find("map_Kd") != string::npos) {
			// 找到map_Kd行
			size_t pos = line.find_last_of("/");
			if (pos != string::npos) {
				// 提取纹理文件名
				string textureFileName = line.substr(pos + 1);
				// 去掉文件名中的后缀名
				size_t dotPos = textureFileName.find_last_of(".");
				if (dotPos != string::npos) {
					textureFileName = textureFileName.substr(0, dotPos);
				}
				// 修改为新的纹理路径
				line = "map_Kd " + textureFileName + ".png";
			}
		}
		// 写入临时文件
		tempFile << line << endl;
	}
	inputFile.close();
	tempFile.close();
	// 删除原始文件
	remove(mtlFile.c_str());
	// 重命名临时文件为原始文件
	rename(tempFileName.c_str(), mtlFile.c_str());
	// cout << "MTL file processed successfully." << endl;
}
// 读取模型
osg::Node* readModel(string inputFilePath)
{
	osg::ref_ptr<osg::Node> node = osgDB::readNodeFile(inputFilePath);
	return node.release();
}
// 移动模型节点到原点
osg::ref_ptr<osg::Node> moveNodeToOrigin(osg::Node* node)
{
	if (!node)
		return nullptr;
	// 获取模型节点的边界球体
	osg::ComputeBoundsVisitor cbv;
	node->accept(cbv);
	osg::BoundingSphere bs = cbv.getBoundingBox();
	// 计算移动的矢量,将模型移动到原点
	osg::Vec3d translation = -bs.center();
	// 创建一个变换节点,用于移动模型
	osg::ref_ptr<osg::PositionAttitudeTransform> transform = new osg::PositionAttitudeTransform;
	transform->setPosition(translation);
	// 将模型添加到变换节点
	transform->addChild(node);
	return transform.release();
}
// 深拷贝并简化新模型
osg::Node* simplityObj(osg::Node* node, float compressLevel)
{
	/*
	创建简化对象
	simplifier(sampleRatio, maxError)
	参数:样本比率、点的误差或边的长度
	样本比率<1 设置点的误差
	样本比率>1 设置边的长度限制
	比率越大,简化越少
	使用的是边塌陷算法
	*/
	float sampleRatio = compressLevel;
	float maxError = 4.0f;
	osgUtil::Simplifier simplifier(sampleRatio, maxError);
	
	//深拷贝
	osg::ref_ptr<osg::Node> deepnode = (osg::Node*)(node->clone(osg::CopyOp::DEEP_COPY_ALL));
	// 旋转节点
	osg::ref_ptr<osg::MatrixTransform> rotationTransform = new osg::MatrixTransform;
	osg::Matrix rotationMatrix;
	// 因为OSG默认会把OBJ绕X轴转动90度,所以要转回去才能正确显示,因此此步骤是将模型绕X轴旋转90度
	rotationMatrix.makeRotate(osg::DegreesToRadians(-90.0), osg::Vec3(1.0, 0.0, 0.0)); 
	rotationTransform->setMatrix(rotationMatrix);
	rotationTransform->addChild(deepnode);
	rotationTransform->accept(simplifier);
	return rotationTransform.release();
}
// 保存纹理图片到指定路径,且指定输出格式为PNG
void saveTextureFile(string directory, string imageName, osg::Image* image)
{
	std::string imagePath = directory + "\\" + imageName + ".png";
	// 使用osgDB库中的写入函数将图像保存为文件
	if (osgDB::writeImageFile(*image, imagePath))
	{
		cout << "Saved image: " << imageName << " to " << imagePath << endl;
	}
	else
	{
		cerr << "Failed to save image: " << imageName << endl;
	}
}
// 检查目录是否存在,如果不存在则创建
bool checkAndCreateDirectory(const std::string& directoryPath) {
	// 使用系统特定的函数检查目录是否存在
#ifdef _WIN32
	if (_mkdir(directoryPath.c_str()) != 0) {
#else
	if (mkdir(directoryPath.c_str(), 0777) != 0) {  // 使用0777权限,可以根据需要进行修改
#endif
		std::cerr << "Error creating directory: " << directoryPath << std::endl;
		return false;
	}
	return true;
}
3. 可执行文件
(1)Windows可执行程序:osgLod.exe

















![[GXYCTF2019]BabySQli 1](https://img-blog.csdnimg.cn/d3182910ce494a248e79e57270df61df.png)


