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

news2025/4/7 3:33:50

文章目录

  • 前言
  • 一、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)其他编译参考:

WindowsLinux
Windows10编译安装OpenSceneGraph(OSG)教程Ubuntu环境OSG的编译、安装与使用
windows平台下用CMake编译osglinux环境编译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 <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


在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1043144.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

记录:移动设备软件开发(Activity的显式启动和隐式启动)

目录 Intent对象简述Intent的作用Intent开启Activtiy显式启动Activity隐式启动Activity Intent对象简述 Android的应用程序包含三种重要组件&#xff1a;Activity、Service、BroadcastReceiver&#xff0c;应用程序采用了一致的方式来启动它们——都是依靠Intent来启动的&…

flv怎么转换成mp4格式?准备3个方法给大家

flv怎么转换成mp4格式&#xff1f;FLV是一种流行的视频文件格式&#xff0c;最初由Adobe公司开发&#xff0c;用于在Web上播放和传输视频内容。FLV格式以其较小的文件大小和较高的压缩比而闻名&#xff0c;并广泛应用于在线视频分享平台、流媒体服务和网络广告等领域。能够提供…

c++图像的边缘检测

图像的边缘检测 cv::Canny 是 OpenCV 中用于进行边缘检测的函数&#xff0c;特别是用于检测图像中的边缘。Canny 边缘检测是一种广泛使用的技术&#xff0c;它能够识别图像中的边缘&#xff0c;这些边缘通常表示对象之间的边界或图像中的显著特征 void cv::Canny(const cv::M…

linux 查看CPU架构是AMD还是ARM

要查看 Linux 系统的 CPU 架构是 AMD 还是 ARM&#xff0c;可以使用以下命令&#xff1a; 使用 lscpu 命令并查找 Architecture 字段&#xff1a; lscpu | grep Architecture如果输出结果中包含 x86_64 或 i686&#xff0c;则表示系统的 CPU 架构是 AMD&#xff08;或者是 x86…

地球红薯地中秋圆《乡村振兴战略下传统村落文化旅游设计》——世界旅行季许少辉八月新书辉少许

地球红薯地中秋圆《乡村振兴战略下传统村落文化旅游设计》——世界旅行季许少辉八月新书辉少许 地球红薯地中秋圆《乡村振兴战略下传统村落文化旅游设计》——世界旅行季许少辉八月新书辉少许

Python中的用法与常见问题解析

装饰器是Python语言中一种强大且常用的概念。通过装饰器&#xff0c;我们可以在不修改原始函数代码的情况下&#xff0c;给函数添加额外的功能&#xff0c;比如日志记录、性能分析、输入验证等。在本文中&#xff0c;我们将深入探讨Python中装饰器的用法和常见问题&#xff0c;…

接口自动化测试:pytest基础讲解

为什么要做接⼝测试&#xff1f; 只靠前端测试很难确保很⾼的覆盖率。接⼝测试&#xff0c;可以模拟出各种类型的⼊参&#xff0c;包括⼀些在前端模拟不出来的⼊参&#xff0c;还能根据接⼝⽂档的定义&#xff0c;设计出相对完善的⼊参值&#xff0c;在接⼝层保证质量&#xff…

@PostMapping‘ not applicable to type 这个是什么原因

PostMapping’ not applicable to type 这个是什么原因 这个错误的意思是 ‘PostMapping’ 注解没有被正确地应用到一个合适的元素上。在Spring MVC中&#xff0c;PostMapping通常用于注解一个处理HTTP POST请求的方法。 出现这个错误&#xff0c;可能的原因有&#xff1a; …

【STM32】读写内部Flash初步使用

基于stm32f103&#xff0c;作为个人学习记录使用 STM32 芯片内部有一个 FLASH 存储器&#xff0c;它主要用于存储代码,在紧急状态下常常会使用内部 FLASH 存储关键记录&#xff1b; 内部 FLASH 的构成 STM32 的内部 FLASH 包含主存储器、系统存储器以及选项字节区域 大容量…

手机相机系统介绍

目录 一张照片是如何生成的? 相机的成像原理 相机硬件 颜色四要素 相机硬件三大块 模组结构 镜头 镜头光路 镜头常见参数 镜头-FOV&EFL 镜头-焦距 镜头-光圈 图像传感器 图像传感器-像素-底 RGB排布 图像传感器-Pattern & PDAF Sensor CMOS sensor …

Python中的单元测试与代码覆盖率:实践与问题解决

当我们开发软件时&#xff0c;单元测试和代码覆盖率是非常重要的工具。它们可以帮助我们验证代码的正确性&#xff0c;并确保代码的质量和稳定性。在Python中&#xff0c;我们有很多强大的工具和库来进行单元测试和代码覆盖率分析。本文将向你分享在Python中进行单元测试和代码…

C++单例模式各种实现方式,终极版即简单又线程安全,无脑用就完了

&#x1f4cb; 前言 &#x1f5b1; 博客主页&#xff1a;在下马农的碎碎念&#x1f917; 欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd;✍ 本文由在下马农原创&#xff0c;首发于CSDN&#x1f4c6; 首发时间&#xff1a;2023/8/25&#x1f4c5; 最近更新时…

学生宿舍管理系统(前端java+后端Vue)实现-含前端与后端程序

界面介绍 登录 ###宿舍管理 ###菜单管理 ###角色管理 ###班级管理

编程每日一练(多语言实现):判断偶数

文章目录 一、实例描述二、技术要点三、代码实现3.1 C 语言实现3.2 Python 语言实现3.3 Java 语言实现 一、实例描述 利用单条件单分支选择语句判断输入的一个整数 是否是偶数。 运行程序&#xff0c;输入一个 整数18&#xff0c; 然后按回车键&#xff0c;将提示该数字是偶数…

性能压力测试的定义及步骤是什么

在今天的数字化时代&#xff0c;软件系统的性能和稳定性对于企业的成功至关重要。为了确保软件在高负载和压力情况下的正常运行&#xff0c;性能压力测试成为了不可或缺的环节。本文将介绍性能压力测试的定义、步骤。 一、性能压力测试的定义和目标 性能压力测试是通过模拟实际…

Spring整合RabbitMQ——生产者

添加依赖坐标&#xff0c;在producer和consumer模块的pom文件中各复制一份。 配置producer的配置文件 配置producer的xml配置文件 编写测试类发送消息

[GXYCTF2019]BabySQli 1

进去就是两个登录框 先试了试adminadmin&#xff0c;然后显示wrong pass 试了下万能密码 1 or 11 查看下页面源代码 放到瑞士军刀解密一下 用fuzz字典跑一下 会发现order被过滤了 所以order用Order来过滤 admin Order by 3-- 得到字段数为3 然后判断一下注入点 判断得到这题…

ESD门禁闸机的用途及优点

ESD门禁闸机是一种专门用于防止静电干扰的门禁设备&#xff0c;其主要用途包括&#xff1a; 防止静电干扰&#xff1a;ESD门禁闸机可以有效地防止静电干扰&#xff0c;保护电子元器件、电路板等敏感设备不受静电破坏。 控制人员进出&#xff1a;ESD门禁闸机可以通过身份验证等…

后端配置(宝塔):处理php禁用函数

一、找到php的文件路径 在软件商店中&#xff0c;找到已安装文件&#xff0c;选择需要更改的php文件&#xff0c;选择“设置” 二、选择需要取消禁用的文件进行删除 扩展&#xff1a;可解决 The Process class relies on proc_open, which is not available on your PHP i nst…

deepin DTK(Development ToolKit)已正式适配 Qt6!

导读近日&#xff0c;深度 deepin 宣布 deepin DTK&#xff08;Development ToolKit&#xff09;已正式适配 Qt6 (6.4.2)&#xff0c;实现全面升级。 DTK 作为 deepin 基于 Qt 开发的一整套简单且实用的通用开发框架&#xff0c;处于 deepin 操作系统中的核心位置&#xff0c;此…