基于OpenSceneGraph的三维模型格式转换(以OBJ为例),并简化、输出纹理图片到指定目录(附完整C++代码和exe)

文章目录

  • 前言
  • 一、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

  1. 不转换出纹理
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. 项目环境

  1. Visual Studio 2022.
  2. OpenSceneGraph3.6.5 Windows编译完成版
  3. 项目配置参考:osg开发配置与第一个osg程序、OSG环境部署(VS2017+WIN10+OSG3.6.5)

2. 完整代码

// 基于OpenSceneGraph开源库,将其他模型转换为OBJ并输出纹理图片到指定路径,包括简化OBJ
// Operating System: Windows 10
#include 
#include 
#include 
#include 
#include 
#include  
#include 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include

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 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


在这里插入图片描述

你可能感兴趣的:(笔记,c++,OpenSceneGraph,Obj,三维模型数据)