Spaces:
Sleeping
Sleeping
| // STB_IMAGE_WRITE is NOT implemented by tinygltf automatically, we must define | |
| // it. | |
| // constant for meters to inches conversion | |
| const double kMetersToInches = 39.3701; | |
| // Helper to get data from accessor | |
| template <typename T> | |
| std::vector<T> GetData(const tinygltf::Model &model, | |
| const tinygltf::Accessor &accessor) { | |
| if (accessor.bufferView < 0) { | |
| std::cerr << "GetData: ERROR - accessor " << accessor.name | |
| << " has no bufferView (index=" << accessor.bufferView << ")" | |
| << std::endl; | |
| return std::vector<T>(); | |
| } | |
| std::cout << "GetData: accessor=" << accessor.bufferView | |
| << " count=" << accessor.count << " stride=" | |
| << accessor.ByteStride(model.bufferViews[accessor.bufferView]) | |
| << std::endl; | |
| const tinygltf::BufferView &bufferView = | |
| model.bufferViews[accessor.bufferView]; | |
| const tinygltf::Buffer &buffer = model.buffers[bufferView.buffer]; | |
| // Check bounds | |
| if (bufferView.byteOffset + accessor.byteOffset + accessor.count * sizeof(T) > | |
| buffer.data.size()) { | |
| // Only strict if stride is tight, but rough check | |
| std::cerr << "WARNING: Accessor points outside buffer!" << std::endl; | |
| } | |
| const unsigned char *data = | |
| buffer.data.data() + bufferView.byteOffset + accessor.byteOffset; | |
| std::vector<T> output; | |
| output.resize(accessor.count); | |
| // Stride check | |
| int stride = accessor.ByteStride(bufferView); | |
| if (stride == sizeof(T)) { | |
| memcpy(output.data(), data, accessor.count * sizeof(T)); | |
| } else { | |
| for (size_t i = 0; i < accessor.count; ++i) { | |
| memcpy(&output[i], data + i * stride, sizeof(T)); | |
| } | |
| } | |
| return output; | |
| } | |
| // Simple struct for vec2 and vec3 | |
| struct Vec2 { | |
| float u, v; | |
| }; | |
| struct Vec3 { | |
| float x, y, z; | |
| }; | |
| // Global stats for debug output (declared before CreateMaterialFromGLTF) | |
| int g_totalFaces = 0; | |
| int g_texturedFaces = 0; | |
| int g_totalVertices = 0; | |
| size_t g_totalTextureBytes = 0; | |
| int g_textureWidth = 0; | |
| int g_textureHeight = 0; | |
| // Helper to create SketchUp material from GLTF material | |
| SUMaterialRef | |
| CreateMaterialFromGLTF(const tinygltf::Model &model, int materialIndex, | |
| SUModelRef su_model, | |
| std::vector<SUMaterialRef> &created_materials) { | |
| if (materialIndex < 0 || materialIndex >= model.materials.size()) | |
| return SU_INVALID; | |
| if (SUIsValid(created_materials[materialIndex])) | |
| return created_materials[materialIndex]; | |
| const tinygltf::Material &gltf_mat = model.materials[materialIndex]; | |
| SUMaterialRef mat = SU_INVALID; | |
| SUMaterialCreate(&mat); | |
| // Set Name | |
| std::string matName = gltf_mat.name; | |
| if (matName.empty()) { | |
| matName = "Material_" + std::to_string(materialIndex); | |
| } | |
| SUMaterialSetName(mat, matName.c_str()); | |
| // Set Color | |
| if (gltf_mat.pbrMetallicRoughness.baseColorFactor.size() == 4) { | |
| SUColor color; | |
| color.red = | |
| (SUByte)(gltf_mat.pbrMetallicRoughness.baseColorFactor[0] * 255); | |
| color.green = | |
| (SUByte)(gltf_mat.pbrMetallicRoughness.baseColorFactor[1] * 255); | |
| color.blue = | |
| (SUByte)(gltf_mat.pbrMetallicRoughness.baseColorFactor[2] * 255); | |
| color.alpha = | |
| (SUByte)(gltf_mat.pbrMetallicRoughness.baseColorFactor[3] * 255); | |
| SUMaterialSetColor(mat, &color); | |
| if (color.alpha < 255) { | |
| SUMaterialSetUseOpacity(mat, true); | |
| SUMaterialSetOpacity(mat, | |
| gltf_mat.pbrMetallicRoughness.baseColorFactor[3]); | |
| } | |
| } | |
| // Set Texture | |
| int texIndex = gltf_mat.pbrMetallicRoughness.baseColorTexture.index; | |
| if (texIndex >= 0 && texIndex < model.textures.size()) { | |
| int imgIndex = model.textures[texIndex].source; | |
| if (imgIndex >= 0 && imgIndex < model.images.size()) { | |
| const tinygltf::Image &image = model.images[imgIndex]; | |
| // Check if image data is loaded | |
| if (image.width > 0 && image.height > 0 && !image.image.empty()) { | |
| // Track texture stats | |
| g_textureWidth = image.width; | |
| g_textureHeight = image.height; | |
| // g_totalTextureBytes calculated after writing file | |
| // Write to temp file | |
| std::string tempFileName; | |
| bool isJpeg = | |
| (image.mimeType == "image/jpeg" || image.mimeType == "image/jpg"); | |
| if (isJpeg) { | |
| tempFileName = "temp_" + std::to_string(imgIndex) + ".jpg"; | |
| // Convert RGBA to RGB if needed | |
| if (image.component == 4) { | |
| std::vector<unsigned char> rgb_data(image.width * image.height * 3); | |
| for (size_t i = 0; i < image.width * image.height; ++i) { | |
| rgb_data[i * 3 + 0] = image.image[i * 4 + 0]; | |
| rgb_data[i * 3 + 1] = image.image[i * 4 + 1]; | |
| rgb_data[i * 3 + 2] = image.image[i * 4 + 2]; | |
| } | |
| stbi_write_jpg(tempFileName.c_str(), image.width, image.height, 3, | |
| rgb_data.data(), 85); | |
| } else { | |
| stbi_write_jpg(tempFileName.c_str(), image.width, image.height, | |
| image.component, image.image.data(), 85); | |
| } | |
| } else { | |
| tempFileName = "temp_" + std::to_string(imgIndex) + ".png"; | |
| stbi_write_png(tempFileName.c_str(), image.width, image.height, | |
| image.component, image.image.data(), | |
| image.width * image.component); | |
| } | |
| // ACCURATE SIZE CALCULATION: | |
| // Read the size of the generated file (JPEG/PNG) instead of the raw | |
| // pixel buffer because SketchUp embeds the compressed file. | |
| std::ifstream in(tempFileName, | |
| std::ifstream::ate | std::ifstream::binary); | |
| if (in.is_open()) { | |
| g_totalTextureBytes += in.tellg(); | |
| } | |
| SUTextureRef texture = SU_INVALID; | |
| SUResult texRes = | |
| SUTextureCreateFromFile(&texture, tempFileName.c_str(), 1.0, 1.0); | |
| if (texRes == SU_ERROR_NONE) { | |
| SUMaterialSetTexture(mat, texture); | |
| } else { | |
| std::cerr << "Failed to create texture from file: " << tempFileName | |
| << std::endl; | |
| } | |
| } | |
| } | |
| } | |
| // Add material to model | |
| SUModelAddMaterials(su_model, 1, &mat); | |
| created_materials[materialIndex] = mat; | |
| return mat; | |
| } | |
| void ProcessNode(const tinygltf::Model &model, const tinygltf::Node &node, | |
| SUEntitiesRef entities, SUModelRef su_model, | |
| std::vector<SUMaterialRef> &created_materials) { | |
| if (node.mesh >= 0) { | |
| if (node.mesh >= model.meshes.size()) { | |
| std::cerr << "Error: Mesh index out of bounds: " << node.mesh | |
| << std::endl; | |
| return; | |
| } | |
| const tinygltf::Mesh &mesh = model.meshes[node.mesh]; | |
| std::cout << "Processing Mesh: " << mesh.name << std::endl; | |
| for (const auto &primitive : mesh.primitives) { | |
| SUGeometryInputRef input = SU_INVALID; | |
| SUGeometryInputCreate(&input); | |
| // Vertices | |
| std::vector<SUPoint3D> su_vertices; | |
| if (primitive.attributes.find("POSITION") != primitive.attributes.end()) { | |
| const tinygltf::Accessor &accessor = | |
| model.accessors[primitive.attributes.at("POSITION")]; | |
| std::vector<Vec3> positions = GetData<Vec3>(model, accessor); | |
| if (positions.empty()) { | |
| std::cerr << "Error: No vertices retrieved for mesh " << mesh.name | |
| << std::endl; | |
| SUGeometryInputRelease(&input); | |
| continue; | |
| } | |
| for (const auto &p : positions) { | |
| SUPoint3D pt; | |
| // GLB uses Y-up coordinate system | |
| // SketchUp uses Z-up coordinate system | |
| // Transformation: GLB(x,y,z) -> SKU(x, -z, y) | |
| // X stays X, GLB Y (up) becomes SKU Z (up), GLB Z (forward) becomes | |
| // SKU -Y (forward) | |
| pt.x = p.x * kMetersToInches; | |
| pt.y = -p.z * kMetersToInches; // GLB Z (forward) becomes SKU -Y | |
| pt.z = | |
| p.y * kMetersToInches; // GLB Y (up) becomes SKU Z (NO negation) | |
| su_vertices.push_back(pt); | |
| } | |
| g_totalVertices += su_vertices.size(); | |
| SUGeometryInputSetVertices(input, su_vertices.size(), | |
| su_vertices.data()); | |
| } | |
| // UVs | |
| std::vector<SUPoint2D> su_uvs; | |
| bool hasUVs = false; | |
| if (primitive.attributes.find("TEXCOORD_0") != | |
| primitive.attributes.end()) { | |
| const tinygltf::Accessor &accessor = | |
| model.accessors[primitive.attributes.at("TEXCOORD_0")]; | |
| std::vector<Vec2> uvs = GetData<Vec2>(model, accessor); | |
| for (const auto &uv : uvs) { | |
| SUPoint2D pt; | |
| pt.x = uv.u; | |
| pt.y = 1.0f - uv.v; // Flip V for SketchUp | |
| su_uvs.push_back(pt); | |
| } | |
| hasUVs = true; | |
| } | |
| std::cout << " - Accessor: " << primitive.indices | |
| << " Mode: " << primitive.mode << std::endl; | |
| if (primitive.mode != TINYGLTF_MODE_TRIANGLES) { | |
| std::cerr | |
| << "WARNING: Primitive mode " << primitive.mode | |
| << " is not TRIANGLES (4). SketchUp converter expects TRIANGLES." | |
| << std::endl; | |
| } | |
| // Indices (Faces) | |
| std::vector<size_t> indices; | |
| if (primitive.indices >= 0) { | |
| const tinygltf::Accessor &indexAccessor = | |
| model.accessors[primitive.indices]; | |
| if (indexAccessor.componentType == | |
| TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { | |
| std::vector<unsigned short> raw = | |
| GetData<unsigned short>(model, indexAccessor); | |
| for (auto v : raw) | |
| indices.push_back(v); | |
| } else if (indexAccessor.componentType == | |
| TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { | |
| std::vector<unsigned int> raw = | |
| GetData<unsigned int>(model, indexAccessor); | |
| for (auto v : raw) | |
| indices.push_back(v); | |
| } else if (indexAccessor.componentType == | |
| TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) { | |
| std::vector<unsigned char> raw = | |
| GetData<unsigned char>(model, indexAccessor); | |
| for (auto v : raw) | |
| indices.push_back(v); | |
| } | |
| } else { | |
| // Handle non-indexed geometry (just vertices) | |
| // Generate sequential indices: 0, 1, 2, ... based on vertex count | |
| size_t vertexCount = su_vertices.size(); | |
| for (size_t i = 0; i < vertexCount; i++) { | |
| indices.push_back(i); | |
| } | |
| } | |
| // Validate indices are within range | |
| size_t maxIndex = su_vertices.size(); | |
| bool validIndices = true; | |
| for (size_t idx : indices) { | |
| if (idx >= maxIndex) { | |
| std::cerr << "Warning: Index " << idx << " out of range (max " | |
| << maxIndex << ")" << std::endl; | |
| validIndices = false; | |
| break; | |
| } | |
| } | |
| if (!validIndices || indices.empty()) { | |
| std::cerr << "Skipping primitive due to invalid indices" << std::endl; | |
| SUGeometryInputRelease(&input); | |
| continue; | |
| } | |
| // Material | |
| SUMaterialRef material = SU_INVALID; | |
| if (primitive.material >= 0) { | |
| material = CreateMaterialFromGLTF(model, primitive.material, su_model, | |
| created_materials); | |
| } | |
| for (size_t i = 0; i + 2 < indices.size(); i += 3) { | |
| size_t i0 = indices[i]; | |
| size_t i1 = indices[i + 1]; | |
| size_t i2 = indices[i + 2]; | |
| // Skip degenerate triangles (same vertex referenced multiple times) | |
| if (i0 == i1 || i1 == i2 || i0 == i2) { | |
| continue; | |
| } | |
| SULoopInputRef loop = SU_INVALID; | |
| SULoopInputCreate(&loop); | |
| // Add vertex indices for the triangle | |
| SULoopInputAddVertexIndex(loop, i0); | |
| SULoopInputAddVertexIndex(loop, i1); | |
| SULoopInputAddVertexIndex(loop, i2); | |
| // Set all edges as SOFT + SMOOTH so the model looks like smooth-shaded | |
| // GLB Edge indices are 0, 1, 2 for the three edges of the triangle This | |
| // makes the model render with smooth shading rather than faceted/boxy | |
| SULoopInputEdgeSetSoft(loop, 0, true); | |
| SULoopInputEdgeSetSmooth(loop, 0, true); | |
| SULoopInputEdgeSetSoft(loop, 1, true); | |
| SULoopInputEdgeSetSmooth(loop, 1, true); | |
| SULoopInputEdgeSetSoft(loop, 2, true); | |
| SULoopInputEdgeSetSmooth(loop, 2, true); | |
| size_t face_index; | |
| SUResult addResult = SUGeometryInputAddFace(input, &loop, &face_index); | |
| if (addResult != SU_ERROR_NONE) { | |
| // Face creation failed, skip | |
| continue; | |
| } | |
| g_totalFaces++; | |
| // Set Material and UVs | |
| if (SUIsValid(material)) { | |
| SUMaterialInput mat_input; | |
| mat_input.material = material; | |
| mat_input.num_uv_coords = 0; | |
| if (hasUVs && i0 < su_uvs.size() && i1 < su_uvs.size() && | |
| i2 < su_uvs.size()) { | |
| mat_input.num_uv_coords = 3; | |
| mat_input.uv_coords[0] = su_uvs[i0]; | |
| mat_input.uv_coords[1] = su_uvs[i1]; | |
| mat_input.uv_coords[2] = su_uvs[i2]; | |
| mat_input.vertex_indices[0] = i0; | |
| mat_input.vertex_indices[1] = i1; | |
| mat_input.vertex_indices[2] = i2; | |
| } | |
| SUGeometryInputFaceSetFrontMaterial(input, face_index, &mat_input); | |
| SUGeometryInputFaceSetBackMaterial(input, face_index, &mat_input); | |
| g_texturedFaces++; | |
| } else if (hasUVs) { | |
| // Even without a material, we might want to apply UVs? | |
| // SketchUp usually ties UVs to a material. | |
| // If no material, no texture to map. So skipping. | |
| } | |
| } | |
| // Fill entities (per primitive) - INSIDE the primitive loop | |
| SUEntitiesFill(entities, input, true); | |
| SUGeometryInputRelease(&input); | |
| } | |
| } | |
| // Children - INSIDE ProcessNode function | |
| for (int childIndex : node.children) { | |
| ProcessNode(model, model.nodes[childIndex], entities, su_model, | |
| created_materials); | |
| } | |
| } | |
| int main(int argc, char **argv) { | |
| if (argc < 3) { | |
| std::cerr << "Usage: " << argv[0] << " input.glb output.skp" << std::endl; | |
| return 1; | |
| } | |
| SUInitialize(); | |
| std::string inputPath = argv[1]; | |
| std::string outputPath = argv[2]; | |
| tinygltf::Model model; | |
| tinygltf::TinyGLTF loader; | |
| // Set tolerent image loader | |
| loader.SetImageLoader( | |
| [](tinygltf::Image *image, const int image_idx, std::string *err, | |
| std::string *warn, int req_width, int req_height, | |
| const unsigned char *bytes, int size, void *user_data) { | |
| bool res = | |
| tinygltf::LoadImageData(image, image_idx, err, warn, req_width, | |
| req_height, bytes, size, user_data); | |
| if (!res) { | |
| // If default loader fails, consume the error and return true to | |
| // proceed without this image | |
| if (warn) { | |
| *warn += "Failed to load image: " + (err ? *err : "Unknown error") + | |
| ". Skipping.\n"; | |
| } | |
| if (err) { | |
| *err = ""; // Clear error | |
| } | |
| return true; | |
| } | |
| return true; | |
| }, | |
| nullptr); | |
| std::string err; | |
| std::string warn; | |
| bool ret = loader.LoadBinaryFromFile(&model, &err, &warn, inputPath); | |
| if (!warn.empty()) { | |
| std::cerr << "Warn: " << warn << std::endl; | |
| } | |
| if (!err.empty()) { | |
| std::cerr << "Err: " << err << std::endl; | |
| } | |
| if (!ret) { | |
| std::cerr << "Failed to load GLTF" << std::endl; | |
| return 1; | |
| } | |
| SUModelRef su_model = SU_INVALID; | |
| SUResult res = SUModelCreate(&su_model); | |
| if (res != SU_ERROR_NONE) | |
| return 1; | |
| // Get model's root entities first (we need it to add the group) | |
| SUEntitiesRef model_entities = SU_INVALID; | |
| SUModelGetEntities(su_model, &model_entities); | |
| // Create a group to hold all geometry (makes it a single unit in SketchUp) | |
| SUGroupRef group = SU_INVALID; | |
| SUGroupCreate(&group); | |
| // Add the group to the model FIRST (required before filling its entities) | |
| SUEntitiesAddGroup(model_entities, group); | |
| // Now get the group's internal entities to add geometry | |
| SUEntitiesRef group_entities = SU_INVALID; | |
| SUGroupGetEntities(group, &group_entities); | |
| // Cache to store created SU materials to reuse them | |
| std::vector<SUMaterialRef> created_materials(model.materials.size(), | |
| SU_INVALID); | |
| // Iterate over scenes | |
| // Iterate over scenes | |
| int sceneindex = (model.defaultScene >= 0) ? model.defaultScene : 0; | |
| if (sceneindex >= 0 && sceneindex < model.scenes.size()) { | |
| const tinygltf::Scene &scene = model.scenes[sceneindex]; | |
| for (size_t i = 0; i < scene.nodes.size(); i++) { | |
| ProcessNode(model, model.nodes[scene.nodes[i]], model_entities, su_model, | |
| created_materials); | |
| } | |
| } else { | |
| // If no valid scene found, try processing all nodes (fallback for flat | |
| // GLBs) | |
| for (size_t i = 0; i < model.nodes.size(); i++) { | |
| ProcessNode(model, model.nodes[i], model_entities, su_model, | |
| created_materials); | |
| } | |
| } | |
| // POST-PROCESSING: Soften ALL edges to make the model | |
| // appear smooth | |
| // This is crucial for imported meshes - it hides all triangle edges | |
| // and makes SketchUp render the model with smooth shading | |
| size_t edge_count = 0; | |
| SUEntitiesGetNumEdges(group_entities, false, &edge_count); | |
| if (edge_count > 0) { | |
| std::vector<SUEdgeRef> edges(edge_count); | |
| size_t actual_count = 0; | |
| SUEntitiesGetEdges(group_entities, false, edge_count, edges.data(), | |
| &actual_count); | |
| // Soften and smooth ALL edges - this makes curved surfaces look smooth | |
| for (size_t i = 0; i < actual_count; i++) { | |
| SUEdgeSetSoft(edges[i], true); | |
| SUEdgeSetSmooth(edges[i], true); | |
| } | |
| std::cerr << "Softened " << actual_count << " edges for smooth appearance" | |
| << std::endl; | |
| } | |
| // Lock the group to prevent accidental exploding | |
| SUComponentInstanceRef group_instance = SUGroupToComponentInstance(group); | |
| if (group_instance.ptr != | |
| 0) { // Check if valid (though SUGroupRef is basically a subclass) | |
| SUComponentInstanceSetLocked(group_instance, true); | |
| } | |
| res = SUModelSaveToFile(su_model, outputPath.c_str()); | |
| if (res != SU_ERROR_NONE) { | |
| std::cerr << "Failed to save SKP file" << std::endl; | |
| } else { | |
| std::cout << "Saved " << outputPath << std::endl; | |
| } | |
| // Cleanup images (optional, if we track temp files) | |
| SUModelRelease(&su_model); | |
| SUTerminate(); | |
| // Calculate size breakdown estimates (in bytes) | |
| // Geometry: ~24 bytes per vertex (3x float position + 3x float normal + 2x | |
| // float UV) Face index: ~12 bytes per face (3x int32 indices) | |
| size_t estimatedGeometryBytes = (g_totalVertices * 24) + (g_totalFaces * 12); | |
| // Calculate SKP Overhead | |
| std::ifstream f(outputPath, std::ifstream::ate | std::ifstream::binary); | |
| size_t actualFileSize = f.tellg(); | |
| f.close(); | |
| long long overhead = (long long)actualFileSize - | |
| (long long)estimatedGeometryBytes - | |
| (long long)g_totalTextureBytes; | |
| if (overhead < 0) | |
| overhead = 0; // Should not happen unless estimates are way off | |
| // Output JSON stats for frontend to parse | |
| std::cout << "{" << std::endl; | |
| std::cout << " \"status\": \"success\"," << std::endl; | |
| std::cout << " \"vertices\": " << g_totalVertices << "," << std::endl; | |
| std::cout << " \"faces\": " << g_totalFaces << "," << std::endl; | |
| std::cout << " \"textured_faces\": " << g_texturedFaces << "," << std::endl; | |
| std::cout << " \"materials\": " << model.materials.size() << "," | |
| << std::endl; | |
| std::cout << " \"textures\": " << model.textures.size() << "," << std::endl; | |
| std::cout << " \"texture_width\": " << g_textureWidth << "," << std::endl; | |
| std::cout << " \"texture_height\": " << g_textureHeight << "," << std::endl; | |
| std::cout << " \"texture_bytes\": " << g_totalTextureBytes << "," | |
| << std::endl; | |
| std::cout << " \"estimated_geometry_bytes\": " << estimatedGeometryBytes | |
| << "," << std::endl; | |
| std::cout << " \"skp_overhead_bytes\": " << overhead << std::endl; | |
| std::cout << "}" << std::endl; | |
| return 0; | |
| } | |