#define TINYGLTF_IMPLEMENTATION #define STB_IMAGE_IMPLEMENTATION #define STB_IMAGE_WRITE_IMPLEMENTATION // STB_IMAGE_WRITE is NOT implemented by tinygltf automatically, we must define // it. #include "vendor/tiny_gltf.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // constant for meters to inches conversion const double kMetersToInches = 39.3701; // Helper to get data from accessor template std::vector 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(); } 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 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 &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 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 &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 su_vertices; if (primitive.attributes.find("POSITION") != primitive.attributes.end()) { const tinygltf::Accessor &accessor = model.accessors[primitive.attributes.at("POSITION")]; std::vector positions = GetData(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 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 uvs = GetData(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 indices; if (primitive.indices >= 0) { const tinygltf::Accessor &indexAccessor = model.accessors[primitive.indices]; if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { std::vector raw = GetData(model, indexAccessor); for (auto v : raw) indices.push_back(v); } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { std::vector raw = GetData(model, indexAccessor); for (auto v : raw) indices.push_back(v); } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) { std::vector raw = GetData(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 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 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; }