Files
OrcaSlicer/src/libslic3r/Format/OBJ.cpp
T
HanifKoh 8ffd3e514e Harden OBJ and DRC Import Against Malformed Files (#15948)
* Validate OBJ Texture-Coordinate Indices

load_obj read the texture coordinates of a face without checking the
vt index, so a face referencing a vt past the end of the list read out
of bounds and crashed, and a face vertex with no vt read index -1.
Out-of-range or missing indices now fall back to a zero UV. The face
keeps its entry in the per-face UV list, so the following faces stay
aligned, and the geometry loads as before.

Negative (relative) vt indices were also rebased by dividing the float
count by 3, but each vt stores two floats.

* Reject DRC Meshes Without Positions or with Invalid Face Indices

load_drc dereferenced the POSITION attribute without checking that the
mesh has one, and trusted the decoded face indices, which the Draco
decoder does not check against the point count. Both now fail the load
cleanly. A failed vertex conversion is treated the same way.

The libslic3r tests link Draco so they can encode the malformed meshes
in-test.

* Keep OBJ Texture Coordinates That Carry a W Component

The vt parser stopped reading the optional third component when texture
coordinates were cut down to u and v, but the check that nothing is left
on the line stayed. A legal "vt u v w" line was therefore rejected and
silently dropped, shifting every later texture index. The w component is
parsed again and discarded.

The texture coordinate stride is now a named constant, OBJ_TEXCOORD_LENGTH,
used by the parser and the importer, so the relative-index rebase cannot
drift from the storage layout again.
2026-09-29 12:25:16 +08:00

405 lines
17 KiB
C++

#include "../libslic3r.h"
#include "../Model.hpp"
#include "../TriangleMesh.hpp"
#include "../TexturePainting.hpp"
#include "ResourcePathUtils.hpp"
#include "OBJ.hpp"
#include "objparser.hpp"
#include <string>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#ifdef _WIN32
#define DIR_SEPARATOR '\\'
#else
#define DIR_SEPARATOR '/'
#endif
//Translation
#include "I18N.hpp"
#define _L(s) Slic3r::I18N::translate(s)
namespace Slic3r {
bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::string &message, ObjParser::MtlData *out_mtl)
{
if (meshptr == nullptr)
return false;
// Parse the OBJ file.
ObjParser::ObjData data;
ObjParser::MtlData mtl_data;
if (! ObjParser::objparse(path, data)) {
BOOST_LOG_TRIVIAL(error) << "load_obj: failed to parse " << path;
message = _L("load_obj: failed to parse");
return false;
}
bool exist_mtl = false;
if (data.mtllibs.size() > 0) { // read mtl
for (auto mtl_name : data.mtllibs) {
if (mtl_name.size() == 0){
continue;
}
exist_mtl = true;
bool mtl_name_is_path = false;
boost::filesystem::path mtl_abs_path(mtl_name);
if (boost::filesystem::exists(mtl_abs_path)) {
mtl_name_is_path = true;
}
boost::filesystem::path mtl_path;
if (!mtl_name_is_path) {
boost::filesystem::path full_path(path);
std::string dir = full_path.parent_path().string();
auto mtl_file = dir + "/" + mtl_name;
boost::filesystem::path temp_mtl_path(mtl_file);
mtl_path = temp_mtl_path;
}
const std::string _mtl_path = (mtl_name_is_path ? mtl_abs_path : mtl_path).string();
if (boost::filesystem::exists(mtl_name_is_path ? mtl_abs_path : mtl_path)) {
if (!ObjParser::mtlparse(_mtl_path.c_str(), mtl_data)) {
BOOST_LOG_TRIVIAL(error) << "load_obj:load_mtl: failed to parse " << _mtl_path;
message = _L("load mtl in obj: failed to parse");
return false;
}
}
else {
BOOST_LOG_TRIVIAL(error) << "load_obj: failed to load mtl_path:" << _mtl_path;
}
}
}
// Count the faces and verify, that all faces are triangular.
size_t num_faces = 0;
size_t num_quads = 0;
for (size_t i = 0; i < data.vertices.size(); ++ i) {
// Find the end of face.
size_t j = i;
for (; j < data.vertices.size() && data.vertices[j].coordIdx != -1; ++ j) ;
if (size_t num_face_vertices = j - i; num_face_vertices > 0) {
if (num_face_vertices > 4) {
// Non-triangular and non-quad faces are not supported as of now.
BOOST_LOG_TRIVIAL(error) << "load_obj: failed to parse " << path << ". The file contains polygons with more than 4 vertices.";
message = _L("The file contains polygons with more than 4 vertices.");
return false;
} else if (num_face_vertices < 3) {
// Non-triangular and non-quad faces are not supported as of now.
BOOST_LOG_TRIVIAL(error) << "load_obj: failed to parse " << path << ". The file contains polygons with less than 2 vertices.";
message = _L("The file contains polygons with less than 2 vertices.");
return false;
}
if (num_face_vertices == 4)
++ num_quads;
++ num_faces;
i = j;
}
}
// Convert ObjData into indexed triangle set.
indexed_triangle_set its;
size_t num_vertices = data.coordinates.size() / OBJ_VERTEX_LENGTH;
its.vertices.reserve(num_vertices);
its.indices.reserve(num_faces + num_quads);
if (exist_mtl) {
obj_info.is_single_mtl = data.usemtls.size() == 1 && mtl_data.new_mtl_unmap.size() == 1;
obj_info.usemtls = data.usemtls;
obj_info.face_colors.reserve(num_faces + num_quads);
}
bool has_color = data.has_vertex_color;
for (size_t i = 0; i < num_vertices; ++ i) {
size_t j = i * OBJ_VERTEX_LENGTH;
its.vertices.emplace_back(data.coordinates[j], data.coordinates[j + 1], data.coordinates[j + 2]);
if (data.has_vertex_color) {
RGBA color{std::clamp(data.coordinates[j + 3], 0.f, 1.f), std::clamp(data.coordinates[j + 4], 0.f, 1.f), std::clamp(data.coordinates[j + 5], 0.f, 1.f),
std::clamp(data.coordinates[j + 6], 0.f, 1.f)};
obj_info.vertex_colors.emplace_back(color);
}
}
int indices[ONE_FACE_SIZE];
int uvs[ONE_FACE_SIZE];
for (size_t i = 0; i < data.vertices.size();)
if (data.vertices[i].coordIdx == -1)
++ i;
else {
int cnt = 0;
while (i < data.vertices.size())
if (const ObjParser::ObjVertex &vertex = data.vertices[i ++]; vertex.coordIdx == -1) {
break;
} else {
assert(cnt < OBJ_VERTEX_LENGTH);
if (vertex.coordIdx < 0 || vertex.coordIdx >= int(its.vertices.size())) {
BOOST_LOG_TRIVIAL(error) << "load_obj: failed to parse " << path << ". The file contains invalid vertex index.";
message = _L("The file contains invalid vertex index.");
return false;
}
indices[cnt] = vertex.coordIdx;
uvs[cnt] = vertex.textureCoordIdx;
cnt++;
}
if (cnt) {
assert(cnt == 3 || cnt == 4);
// Insert one or two faces (triangulate a quad).
its.indices.emplace_back(indices[0], indices[1], indices[2]);
int face_index =its.indices.size() - 1;
RGBA face_color;
auto set_face_color = [&uvs, &data, &mtl_data, &obj_info, &face_color](int face_index, const std::string mtl_name) {
if (mtl_data.new_mtl_unmap.find(mtl_name) != mtl_data.new_mtl_unmap.end()) {
bool is_merge_ka_kd = true;
for (size_t n = 0; n < 3; n++) {
if (float(mtl_data.new_mtl_unmap[mtl_name]->Ka[n] + mtl_data.new_mtl_unmap[mtl_name]->Kd[n]) > 1.0) {
is_merge_ka_kd=false;
break;
}
}
for (size_t n = 0; n < 3; n++) {
if (is_merge_ka_kd) {
face_color[n] = std::clamp(float(mtl_data.new_mtl_unmap[mtl_name]->Ka[n] + mtl_data.new_mtl_unmap[mtl_name]->Kd[n]), 0.f, 1.f);
}
else {
face_color[n] = std::clamp(float(mtl_data.new_mtl_unmap[mtl_name]->Kd[n]), 0.f, 1.f);
}
}
face_color[3] = mtl_data.new_mtl_unmap[mtl_name]->Tr; // alpha
if (mtl_data.new_mtl_unmap[mtl_name]->map_Kd.size() > 0) {
auto png_name = mtl_data.new_mtl_unmap[mtl_name]->map_Kd;
obj_info.has_uv_png = true;
if (obj_info.pngs.find(png_name) == obj_info.pngs.end()) { obj_info.pngs[png_name] = false; }
obj_info.uv_map_pngs[face_index] = png_name;
}
if (data.textureCoordinates.size() > 0) {
// A face vertex may omit vt or reference a missing one. Fall back to (0, 0) rather than
// skipping the face, so obj_info.uvs stays aligned with the face indices.
const int uv_count = static_cast<int>(data.textureCoordinates.size() / OBJ_TEXCOORD_LENGTH);
auto uv_at = [&data, uv_count](int idx) -> Vec2f {
if (idx < 0 || idx >= uv_count)
return Vec2f::Zero();
return Vec2f(data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH], data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH + 1]);
};
std::array<Vec2f, 3> uv_array{uv_at(uvs[0]), uv_at(uvs[1]), uv_at(uvs[2])};
obj_info.uvs.emplace_back(uv_array);
}
obj_info.face_colors.emplace_back(face_color);
}
else {
if (obj_info.lost_material_name.empty()) {
obj_info.lost_material_name = mtl_name;
}
}
};
auto set_face_color_by_mtl = [&data, &set_face_color](int face_index) {
if (data.usemtls.size() == 1) {
set_face_color(face_index, data.usemtls[0].name);
} else {
for (size_t k = 0; k < data.usemtls.size(); k++) {
auto mtl = data.usemtls[k];
if (face_index >= mtl.face_start && face_index <= mtl.face_end) {
set_face_color(face_index, data.usemtls[k].name);
break;
}
}
}
};
if (exist_mtl) {
set_face_color_by_mtl(face_index);
}
if (cnt == 4) {
its.indices.emplace_back(indices[0], indices[2], indices[3]);
int face_index = its.indices.size() - 1;
if (exist_mtl) {
set_face_color_by_mtl(face_index);
}
}
}
}
*meshptr = TriangleMesh(std::move(its));
if (meshptr->empty()) {
BOOST_LOG_TRIVIAL(error) << "load_obj: This OBJ file couldn't be read because it's empty. " << path;
message = _L("This OBJ file couldn't be read because it's empty.");
return false;
}
if (meshptr->volume() < 0)
meshptr->flip_triangles();
// Hand the parsed material table back so callers can build a TexturedMesh from it.
if (out_mtl)
*out_mtl = mtl_data;
return true;
}
bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &message, const char *object_name_in, ObjParser::MtlData *out_mtl)
{
TriangleMesh mesh;
bool ret = load_obj(path, &mesh, obj_info, message, out_mtl);
if (ret) {
std::string object_name;
if (object_name_in == nullptr) {
const char *last_slash = strrchr(path, DIR_SEPARATOR);
object_name.assign((last_slash == nullptr) ? path : last_slash + 1);
} else
object_name.assign(object_name_in);
model->add_object(object_name.c_str(), path, std::move(mesh));
}
return ret;
}
bool obj_to_textured_mesh(
const ObjInfo& obj_info,
const indexed_triangle_set& its,
const ObjParser::MtlData& mtl_data,
const std::string& obj_directory,
TexturedMesh& out)
{
if (its.vertices.empty() || its.indices.empty() || !obj_info.has_uv_png)
return false;
const size_t nv = its.vertices.size();
const size_t nf = its.indices.size();
// 1. Copy vertices
out.vertices.resize(nv);
for (size_t i = 0; i < nv; ++i)
out.vertices[i] = {its.vertices[i].x(), its.vertices[i].y(), its.vertices[i].z()};
// 2. Copy face indices
out.indices.resize(nf);
for (size_t i = 0; i < nf; ++i)
out.indices[i] = {its.indices[i][0], its.indices[i][1], its.indices[i][2]};
// 3. Build per-face UV (uv_coords + uv_indices)
// OBJ UV convention: V=0 at bottom (OpenGL); texture sampling expects V=0 at top (like glTF/OpenCV).
// Flip V here so downstream code works uniformly.
if (!obj_info.uvs.empty()) {
const size_t uv_face_count = obj_info.uvs.size();
out.uv_coords.resize(uv_face_count * 3);
out.uv_indices.resize(nf);
for (size_t fi = 0; fi < nf; ++fi) {
if (fi < uv_face_count) {
int base = static_cast<int>(fi * 3);
out.uv_coords[base + 0] = {obj_info.uvs[fi][0].x(), 1.f - obj_info.uvs[fi][0].y()};
out.uv_coords[base + 1] = {obj_info.uvs[fi][1].x(), 1.f - obj_info.uvs[fi][1].y()};
out.uv_coords[base + 2] = {obj_info.uvs[fi][2].x(), 1.f - obj_info.uvs[fi][2].y()};
out.uv_indices[fi] = {base, base + 1, base + 2};
} else {
out.uv_indices[fi] = {0, 0, 0};
}
}
}
// 4. Build material list and load textures from disk
// Map: material name -> material index
std::map<std::string, int> mtl_name_to_idx;
for (size_t i = 0; i < mtl_data.mtl_orders.size(); ++i)
mtl_name_to_idx[mtl_data.mtl_orders[i]] = static_cast<int>(i);
const int num_materials = static_cast<int>(mtl_data.mtl_orders.size());
out.material_colors.resize(num_materials, {1.f, 1.f, 1.f, 1.f});
out.material_texture_map.resize(num_materials, -1);
// Map: texture filename -> index in out.textures
std::map<std::string, int> png_to_tex_idx;
for (int mi = 0; mi < num_materials; ++mi) {
const std::string& name = mtl_data.mtl_orders[mi];
auto it = mtl_data.new_mtl_unmap.find(name);
if (it == mtl_data.new_mtl_unmap.end())
continue;
const auto& mtl = *(it->second);
// Material color from Kd
out.material_colors[mi] = {mtl.Kd[0], mtl.Kd[1], mtl.Kd[2], mtl.Tr};
// Texture from map_Kd
if (mtl.map_Kd.empty())
continue;
auto tex_it = png_to_tex_idx.find(mtl.map_Kd);
if (tex_it != png_to_tex_idx.end()) {
out.material_texture_map[mi] = tex_it->second;
continue;
}
// Resolve texture file path.
const boost::filesystem::path requested_tex_path(mtl.map_Kd);
const boost::filesystem::path tex_path = requested_tex_path.is_absolute() ?
resource_path::resolve_existing_path_case_insensitive(requested_tex_path, "obj_to_textured_mesh: map_Kd") :
resource_path::resolve_existing_relative_path_case_insensitive(
boost::filesystem::path(obj_directory), requested_tex_path, "obj_to_textured_mesh: map_Kd");
if (tex_path.empty()) {
BOOST_LOG_TRIVIAL(warning) << "obj_to_textured_mesh: texture not found: " << requested_tex_path;
continue;
}
// Read raw file bytes
boost::nowide::ifstream file(tex_path.string(), std::ios::binary | std::ios::ate);
if (!file.is_open())
continue;
auto file_size = file.tellg();
if (file_size <= 0)
continue;
file.seekg(0, std::ios::beg);
TextureImage ti;
ti.data.resize(static_cast<size_t>(file_size));
file.read(reinterpret_cast<char*>(ti.data.data()), file_size);
ti.width = -1;
ti.height = -1;
ti.channels = 0;
int new_idx = static_cast<int>(out.textures.size());
out.textures.push_back(std::move(ti));
png_to_tex_idx[mtl.map_Kd] = new_idx;
out.material_texture_map[mi] = new_idx;
}
// 5. Build per-face material_ids from usemtls ranges
out.material_ids.resize(nf, -1);
if (!obj_info.usemtls.empty()) {
for (size_t fi = 0; fi < nf; ++fi) {
int face_idx = static_cast<int>(fi);
for (size_t k = 0; k < obj_info.usemtls.size(); ++k) {
const auto& um = obj_info.usemtls[k];
if (face_idx >= um.face_start && face_idx <= um.face_end) {
auto name_it = mtl_name_to_idx.find(um.name);
if (name_it != mtl_name_to_idx.end())
out.material_ids[fi] = name_it->second;
break;
}
}
}
}
if (out.textures.empty()) {
BOOST_LOG_TRIVIAL(warning) << "obj_to_textured_mesh: no textures loaded";
return false;
}
BOOST_LOG_TRIVIAL(info) << "obj_to_textured_mesh: " << nf << " faces, "
<< out.textures.size() << " textures, "
<< num_materials << " materials";
return true;
}
bool store_obj(const char *path, TriangleMesh *mesh)
{
//FIXME returning false even if write failed.
mesh->WriteOBJFile(path);
return true;
}
bool store_obj(const char *path, ModelObject *model_object)
{
TriangleMesh mesh = model_object->mesh();
return store_obj(path, &mesh);
}
bool store_obj(const char *path, Model *model)
{
TriangleMesh mesh = model->mesh();
return store_obj(path, &mesh);
}
}; // namespace Slic3r