本文基于OpenSceneGraph,使用C++实现其他三维模型格式到OBJ的转换,并简化、输出纹理图片到指定目录。
(1)中文官网:osgChina.org
(2)英文官网:OpenSceneGraph.com
(3)下载源码和第三方库:OpenSceneGraph源码和第三方库
(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 | 非免费 |
(1)OpenScenGraph3.6.5_Windows.zip
(2)说明:上面的版本已经能够满足很多需求,但是FBX插件存在问题,可以下载FBX SDK编译(Autodesk FBX SDK 下载)(Ubuntu编译FBX问题参考)。
提示:osgconv工具在bin目录中
osgconv 主要的功能是用来将3D模型进行格式转换和进行一些诸如纹理压缩类的操作的: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度,详情参看官方文档.
提示:为了解决以上问题,可以调用OSG的dll进行修改,以下内容以将3ds格式数据转换为OBJ,并进行简化,输出纹理到指定目录
// 基于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;
}
(1)Windows可执行程序:osgLod.exe