深度解析:如何高效实现 SKP 到 GLTF 的转换——附带完整源代码
在现代 3D 图形处理中,GLTF 格式因其高效的传输和渲染性能而备受青睐,其格式是公开的。而 SketchUp 的 SKP 格式是封闭的。如何将 SKP 转换为 GLTF 是许多开发者面临的挑战。本文将深入解析如何高效实现 SKP 到 GLTF 的转换,并附带完整的源代码示例。
项目概述
本项目旨在实现一个工具,将 SketchUp 的 SKP 文件转换为 GLTF 格式。项目使用 C++ 编写,并结合了多种第三方库,如 SketchUp SDK、Draco、TinyGLTF 等,以确保转换的高效性和准确性。
项目结构
项目的主要结构如下:
-
CMakeLists.txt : 项目的构建配置文件,定义了项目的编译选项、依赖库和生成目标。
-
include/ : 包含项目的头文件,定义了项目的接口和数据结构。
-
exportgltf/ : GLTF 导出相关头文件。
-
gltflib/ : GLTF 库头文件。
-
skp2xml/ : SKP 到 XML 转换头文件。
-
src/ : 包含项目的源代码,实现了项目的核心功能。
-
exportgltf/ : 实现 GLTF 导出功能。
-
gltflib/ : 实现 GLTF 库功能。
-
skp2xml/ : 实现 SKP 到 XML 转换功能。
-
main.cpp : 项目的主程序入口。
-
thirdparty/ : 包含项目使用的第三方库。
-
SketchUpAPI/ : SketchUp SDK。
-
draco/ : Draco 压缩库。
-
tinygltf/ : GLTF 解析库。
-
其他 JSON 和 XML 解析库。
-
lib/ : 包含项目依赖的库文件。
项目实现逻辑
±-----------------+
| main.cpp |
| (程序入口) |
±-------±--------+
|
v
±-------±--------+
| skp2xml |
| (SKP -> XML) |
±-------±--------+
|
v
±-------±--------+
| exportgltf |
| (XML -> GLTF) |
±-------±--------+
|
v
±-------±--------+
关键技术点
1. 使用 SketchUp SDK 解析 SKP 文件
SketchUp SDK 提供了丰富的 API 用于解析 SKP 文件。通过这些 API,我们可以提取模型的几何信息、材质信息等。
2. 使用 TinyGLTF 生成 GLTF 文件
TinyGLTF 是一个轻量级的 GLTF 解析和生成库。我们使用它来构建 GLTF 文件的结构,并将压缩后的几何数据写入其中。
代码实现
以下是项目的核心代码片段:
// ... existing code ...
void CXmlExporter::exportToGltfImpl(const std::string &gltfName) {
// 处理文件名后缀
std::string outputPath = gltfName;
if (outputPath.length() < 5 || outputPath.substr(outputPath.length() - 5) != ".gltf") {
outputPath += ".gltf";
}
// 初始化 GLTF 模型
tinygltf::Model model;
model.asset.version = "2.0";
model.asset.generator = "zhuzhaoyun";
tinygltf::Scene scene;
model.scenes.push_back(scene);
model.defaultScene = 0;
// 添加一个默认buffer用于存储所有数据
model.buffers.push_back(tinygltf::Buffer());
// 处理几何数据
for (auto &item : facetMap) {
tinygltf::Mesh mesh;
tinygltf::Primitive primitive;
primitive.mode = 4; // triangles
// 收集顶点和索引数据
std::vector<float> positions;
std::vector<float> uvs;
std::vector<unsigned int> indices;
// 添加用于计算边界的变量
float posMin[3] = {FLT_MAX, FLT_MAX, FLT_MAX};
float posMax[3] = {-FLT_MAX, -FLT_MAX, -FLT_MAX};
std::vector<cFacet> &facetVec = item.second;
size_t vertexOffset = 0;
for (size_t i = 0; i < facetVec.size(); i++) {
for (int j = 0; j < 3; j++) {
float x = static_cast<float>(facetVec[i].vertex[j].x);
float y = static_cast<float>(facetVec[i].vertex[j].y);
float z = static_cast<float>(facetVec[i].vertex[j].z);
// 更新边界值
posMin[0] = (std::min)(posMin[0], x);
posMin[1] = (std::min)(posMin[1], y);
posMin[2] = (std::min)(posMin[2], z);
posMax[0] = (std::max)(posMax[0], x);
posMax[1] = (std::max)(posMax[1], y);
posMax[2] = (std::max)(posMax[2], z);
positions.push_back(x);
positions.push_back(y);
positions.push_back(z);
if (!facetVec.empty()) {
uvs.push_back(static_cast<float>(facetVec[i].uv[j].x));
uvs.push_back(static_cast<float>(-facetVec[i].uv[j].y));
}
indices.push_back(vertexOffset++);
}
}
// 创建并添加顶点位置buffer
{
tinygltf::BufferView bufferView;
bufferView.buffer = 0;
bufferView.byteOffset = model.buffers[0].data.size();
bufferView.byteLength = positions.size() * sizeof(float);
bufferView.target = TINYGLTF_TARGET_ARRAY_BUFFER;
size_t bufferOffset = model.buffers[0].data.size();
model.buffers[0].data.resize(bufferOffset + bufferView.byteLength);
memcpy(model.buffers[0].data.data() + bufferOffset, positions.data(), bufferView.byteLength);
int positionBufferViewIndex = model.bufferViews.size();
model.bufferViews.push_back(bufferView);
tinygltf::Accessor accessor;
accessor.bufferView = positionBufferViewIndex;
accessor.byteOffset = 0;
accessor.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT;
accessor.count = positions.size() / 3;
accessor.type = TINYGLTF_TYPE_VEC3;
// 设置边界值
accessor.minValues = {posMin[0], posMin[1], posMin[2]};
accessor.maxValues = {posMax[0], posMax[1], posMax[2]};
int positionAccessorIndex = model.accessors.size();
model.accessors.push_back(accessor);
primitive.attributes["POSITION"] = positionAccessorIndex;
}
// 创建并添加UV buffer (如果有UV数据)
if (!uvs.empty() && !item.first.imageUri.empty()) {
tinygltf::BufferView bufferView;
bufferView.buffer = 0;
bufferView.byteOffset = model.buffers[0].data.size();
bufferView.byteLength = uvs.size() * sizeof(float);
bufferView.target = TINYGLTF_TARGET_ARRAY_BUFFER;
size_t bufferOffset = model.buffers[0].data.size();
model.buffers[0].data.resize(bufferOffset + bufferView.byteLength);
memcpy(model.buffers[0].data.data() + bufferOffset, uvs.data(), bufferView.byteLength);
int uvBufferViewIndex = model.bufferViews.size();
model.bufferViews.push_back(bufferView);
tinygltf::Accessor accessor;
accessor.bufferView = uvBufferViewIndex;
accessor.byteOffset = 0;
accessor.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT;
accessor.count = uvs.size() / 2;
accessor.type = TINYGLTF_TYPE_VEC2;
int uvAccessorIndex = model.accessors.size();
model.accessors.push_back(accessor);
primitive.attributes["TEXCOORD_0"] = uvAccessorIndex;
}
// 创建并添加索引buffer
{
tinygltf::BufferView bufferView;
bufferView.buffer = 0;
bufferView.byteOffset = model.buffers[0].data.size();
bufferView.byteLength = indices.size() * sizeof(unsigned int);
bufferView.target = TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER;
size_t bufferOffset = model.buffers[0].data.size();
model.buffers[0].data.resize(bufferOffset + bufferView.byteLength);
memcpy(model.buffers[0].data.data() + bufferOffset, indices.data(), bufferView.byteLength);
int indexBufferViewIndex = model.bufferViews.size();
model.bufferViews.push_back(bufferView);
tinygltf::Accessor accessor;
accessor.bufferView = indexBufferViewIndex;
accessor.byteOffset = 0;
accessor.componentType = TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT;
accessor.count = indices.size();
accessor.type = TINYGLTF_TYPE_SCALAR;
primitive.indices = model.accessors.size();
model.accessors.push_back(accessor);
}
// 设置材质
tinygltf::Material material;
material.pbrMetallicRoughness.baseColorFactor = {
item.first.r, item.first.g, item.first.b, item.first.a
};
material.name = item.first.name;
material.pbrMetallicRoughness.metallicFactor = 0.0;
material.pbrMetallicRoughness.roughnessFactor = 1.0;
if (!item.first.imageUri.empty()) {
std::string processedTexturePath = ProcessTexture(item.first.imageUri);
tinygltf::Image gltfImage;
gltfImage.uri = processedTexturePath;
int imageIndex = model.images.size();
model.images.push_back(gltfImage);
tinygltf::Texture gltfTexture;
gltfTexture.source = imageIndex;
int textureIndex = model.textures.size();
model.textures.push_back(gltfTexture);
material.pbrMetallicRoughness.baseColorTexture.index = textureIndex;
}
primitive.material = model.materials.size();
model.materials.push_back(material);
mesh.primitives.push_back(primitive);
model.meshes.push_back(mesh);
tinygltf::Node node;
node.mesh = model.meshes.size() - 1;
model.nodes.push_back(node);
model.scenes[0].nodes.push_back(model.nodes.size() - 1);
}
// 写入 GLTF 文件
tinygltf::TinyGLTF gltf;
bool ret = gltf.WriteGltfSceneToFile(&model, outputPath,
true, // embedImages
true, // embedBuffers
true, // prettyPrint
false); // writeBinary
return ret ? 0 : 1;
}
// ... existing code ...
构建和运行
转换的文件显示如下
总结
当前解析完成的gltf 的大小是原 skp 文件的2~3倍。目前 draco 压缩还没有引入,后续引入 draco 压缩,解析的速度上会有一些影响,但 gltf 的大小会大大降低。