OBJ: New C++ based wavefront OBJ importer

This takes state of soc-2020-io-performance branch as it was at
e9bbfd0c8c7 (2021 Oct 31), merges latest master (2022 Apr 4),
adds a bunch of tests, and fixes a bunch of stuff found by said
tests. The fixes are detailed in the differential.

Timings on my machine (Windows, VS2022 release build, AMD Ryzen
5950X 32 threads):

- Rungholt minecraft level (269MB file, 1 mesh): 54.2s -> 14.2s
  (memory usage: 7.0GB -> 1.9GB).
- Blender 3.0 splash scene: "I waited for 90 minutes and gave up"
  -> 109s. Now, this time is not great, but at least 20% of the
  time is spent assigning unique names for the imported objects
  (the scene has 24 thousand objects). This is not specific to obj
  importer, but rather a general issue across blender overall.

Test suite file updates done in Subversion tests repository.

Reviewed By: @howardt, @sybren
Differential Revision: https://developer.blender.org/D13958
This commit is contained in:
Ankit Meel 2022-04-04 13:36:10 +03:00 committed by Aras Pranckevicius
parent ee3f71d747
commit e6a9b22384
23 changed files with 3091 additions and 0 deletions

View File

@ -456,6 +456,7 @@ class TOPBAR_MT_file_import(Menu):
"wm.usd_import", text="Universal Scene Description (.usd, .usdc, .usda)")
self.layout.operator("wm.gpencil_import_svg", text="SVG as Grease Pencil")
self.layout.operator("wm.obj_import", text="Wavefront (.obj) (experimental)")
class TOPBAR_MT_file_export(Menu):

View File

@ -359,3 +359,87 @@ void WM_OT_obj_export(struct wmOperatorType *ot)
RNA_def_boolean(
ot->srna, "smooth_group_bitflags", false, "Generate Bitflags for Smooth Groups", "");
}
static int wm_obj_import_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event))
{
WM_event_add_fileselect(C, op);
return OPERATOR_RUNNING_MODAL;
}
static int wm_obj_import_exec(bContext *C, wmOperator *op)
{
if (!RNA_struct_property_is_set(op->ptr, "filepath")) {
BKE_report(op->reports, RPT_ERROR, "No filename given");
return OPERATOR_CANCELLED;
}
struct OBJImportParams import_params;
RNA_string_get(op->ptr, "filepath", import_params.filepath);
import_params.clamp_size = RNA_float_get(op->ptr, "clamp_size");
import_params.forward_axis = RNA_enum_get(op->ptr, "forward_axis");
import_params.up_axis = RNA_enum_get(op->ptr, "up_axis");
OBJ_import(C, &import_params);
return OPERATOR_FINISHED;
}
static void ui_obj_import_settings(uiLayout *layout, PointerRNA *imfptr)
{
uiLayoutSetPropSep(layout, true);
uiLayoutSetPropDecorate(layout, false);
uiLayout *box = uiLayoutBox(layout);
uiItemL(box, IFACE_("Transform"), ICON_OBJECT_DATA);
uiLayout *col = uiLayoutColumn(box, false);
uiLayout *sub = uiLayoutColumn(col, false);
uiItemR(sub, imfptr, "clamp_size", 0, NULL, ICON_NONE);
sub = uiLayoutColumn(col, false);
uiItemR(sub, imfptr, "forward_axis", 0, IFACE_("Axis Forward"), ICON_NONE);
uiItemR(sub, imfptr, "up_axis", 0, IFACE_("Up"), ICON_NONE);
}
static void wm_obj_import_draw(bContext *C, wmOperator *op)
{
PointerRNA ptr;
wmWindowManager *wm = CTX_wm_manager(C);
RNA_pointer_create(&wm->id, op->type->srna, op->properties, &ptr);
ui_obj_import_settings(op->layout, &ptr);
}
void WM_OT_obj_import(struct wmOperatorType *ot)
{
ot->name = "Import Wavefront OBJ";
ot->description = "Load a Wavefront OBJ scene";
ot->idname = "WM_OT_obj_import";
ot->invoke = wm_obj_import_invoke;
ot->exec = wm_obj_import_exec;
ot->poll = WM_operator_winactive;
ot->ui = wm_obj_import_draw;
WM_operator_properties_filesel(ot,
FILE_TYPE_FOLDER | FILE_TYPE_OBJECT_IO,
FILE_BLENDER,
FILE_OPENFILE,
WM_FILESEL_FILEPATH | WM_FILESEL_SHOW_PROPS,
FILE_DEFAULTDISPLAY,
FILE_SORT_ALPHA);
RNA_def_float(
ot->srna,
"clamp_size",
0.0f,
0.0f,
1000.0f,
"Clamp Bounding Box",
"Resize the objects to keep bounding box under this value. Value 0 diables clamping",
0.0f,
1000.0f);
RNA_def_enum(ot->srna,
"forward_axis",
io_obj_transform_axis_forward,
OBJ_AXIS_NEGATIVE_Z_FORWARD,
"Forward Axis",
"");
RNA_def_enum(ot->srna, "up_axis", io_obj_transform_axis_up, OBJ_AXIS_Y_UP, "Up Axis", "");
}

View File

@ -9,3 +9,4 @@
struct wmOperatorType;
void WM_OT_obj_export(struct wmOperatorType *ot);
void WM_OT_obj_import(struct wmOperatorType *ot);

View File

@ -59,4 +59,5 @@ void ED_operatortypes_io(void)
WM_operatortype_append(CACHEFILE_OT_layer_move);
WM_operatortype_append(WM_OT_obj_export);
WM_operatortype_append(WM_OT_obj_import);
}

View File

@ -3,6 +3,7 @@
set(INC
.
./exporter
./importer
../../blenkernel
../../blenlib
../../bmesh
@ -28,6 +29,13 @@ set(SRC
exporter/obj_export_mtl.cc
exporter/obj_export_nurbs.cc
exporter/obj_exporter.cc
importer/importer_mesh_utils.cc
importer/obj_import_file_reader.cc
importer/obj_import_mesh.cc
importer/obj_import_mtl.cc
importer/obj_import_nurbs.cc
importer/obj_importer.cc
importer/parser_string_utils.cc
IO_wavefront_obj.h
exporter/obj_export_file_writer.hh
@ -36,6 +44,14 @@ set(SRC
exporter/obj_export_mtl.hh
exporter/obj_export_nurbs.hh
exporter/obj_exporter.hh
importer/importer_mesh_utils.hh
importer/obj_import_file_reader.hh
importer/obj_import_mesh.hh
importer/obj_import_mtl.hh
importer/obj_import_nurbs.hh
importer/obj_import_objects.hh
importer/obj_importer.hh
importer/parser_string_utils.hh
)
set(LIB
@ -53,6 +69,7 @@ blender_add_lib(bf_wavefront_obj "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
if(WITH_GTESTS)
set(TEST_SRC
tests/obj_exporter_tests.cc
tests/obj_importer_tests.cc
tests/obj_exporter_tests.hh
)

View File

@ -9,6 +9,7 @@
#include "IO_wavefront_obj.h"
#include "obj_exporter.hh"
#include "obj_importer.hh"
/**
* C-interface for the exporter.
@ -18,3 +19,12 @@ void OBJ_export(bContext *C, const OBJExportParams *export_params)
SCOPED_TIMER("OBJ export");
blender::io::obj::exporter_main(C, *export_params);
}
/**
* Time the full import process.
*/
void OBJ_import(bContext *C, const OBJImportParams *import_params)
{
SCOPED_TIMER(__func__);
blender::io::obj::importer_main(C, *import_params);
}

View File

@ -77,6 +77,17 @@ struct OBJExportParams {
bool smooth_groups_bitflags;
};
struct OBJImportParams {
/** Full path to the source OBJ file to import. */
char filepath[FILE_MAX];
/** Value 0 disables clamping. */
float clamp_size;
eTransformAxisForward forward_axis;
eTransformAxisUp up_axis;
};
void OBJ_import(bContext *C, const struct OBJImportParams *import_params);
void OBJ_export(bContext *C, const struct OBJExportParams *export_params);
#ifdef __cplusplus

View File

@ -0,0 +1,133 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include "BKE_mesh.h"
#include "BKE_object.h"
#include "BLI_delaunay_2d.h"
#include "BLI_math_vector.h"
#include "BLI_set.hh"
#include "DNA_object_types.h"
#include "IO_wavefront_obj.h"
#include "importer_mesh_utils.hh"
namespace blender::io::obj {
Vector<Vector<int>> fixup_invalid_polygon(Span<float3> vertex_coords,
Span<int> face_vertex_indices)
{
using namespace blender::meshintersect;
if (face_vertex_indices.size() < 3) {
return {};
}
/* Calculate face normal, to project verts to 2D. */
float normal[3] = {0, 0, 0};
float3 co_prev = vertex_coords[face_vertex_indices.last()];
for (int idx : face_vertex_indices) {
BLI_assert(idx >= 0 && idx < vertex_coords.size());
float3 co_curr = vertex_coords[idx];
add_newell_cross_v3_v3v3(normal, co_prev, co_curr);
co_prev = co_curr;
}
if (UNLIKELY(normalize_v3(normal) == 0.0f)) {
normal[2] = 1.0f;
}
float axis_mat[3][3];
axis_dominant_v3_to_m3(axis_mat, normal);
/* Prepare data for CDT. */
CDT_input<double> input;
input.vert.reinitialize(face_vertex_indices.size());
input.face.reinitialize(1);
input.face[0].resize(face_vertex_indices.size());
for (int64_t i = 0; i < face_vertex_indices.size(); ++i) {
input.face[0][i] = i;
}
input.epsilon = 1.0e-6f;
input.need_ids = true;
/* Project vertices to 2D. */
for (size_t i = 0; i < face_vertex_indices.size(); ++i) {
int idx = face_vertex_indices[i];
BLI_assert(idx >= 0 && idx < vertex_coords.size());
float3 coord = vertex_coords[idx];
float2 coord2d;
mul_v2_m3v3(coord2d, axis_mat, coord);
input.vert[i] = double2(coord2d.x, coord2d.y);
}
CDT_result<double> res = delaunay_2d_calc(input, CDT_CONSTRAINTS_VALID_BMESH_WITH_HOLES);
/* Emit new face information from CDT result. */
Vector<Vector<int>> faces;
faces.reserve(res.face.size());
for (const auto &f : res.face) {
Vector<int> face_verts;
face_verts.reserve(f.size());
for (int64_t i = 0; i < f.size(); ++i) {
int idx = f[i];
BLI_assert(idx >= 0 && idx < res.vert_orig.size());
if (res.vert_orig[idx].is_empty()) {
/* If we have a whole new vertex in the tessellated result,
* we won't quite know what to do with it (how to create normal/UV
* for it, for example). Such vertices are often due to
* self-intersecting polygons. Just skip them from the output
* polygon. */
}
else {
/* Vertex corresponds to one or more of the input vertices, use it. */
idx = res.vert_orig[idx][0];
BLI_assert(idx >= 0 && idx < face_vertex_indices.size());
face_verts.append(idx);
}
}
faces.append(face_verts);
}
return faces;
}
void transform_object(Object *object, const OBJImportParams &import_params)
{
float axes_transform[3][3];
unit_m3(axes_transform);
float obmat[4][4];
unit_m4(obmat);
/* +Y-forward and +Z-up are the default Blender axis settings. */
mat3_from_axis_conversion(import_params.forward_axis,
import_params.up_axis,
OBJ_AXIS_Y_FORWARD,
OBJ_AXIS_Z_UP,
axes_transform);
/* mat3_from_axis_conversion returns a transposed matrix! */
transpose_m3(axes_transform);
copy_m4_m3(obmat, axes_transform);
BKE_object_apply_mat4(object, obmat, true, false);
if (import_params.clamp_size != 0.0f) {
float3 max_coord(-INT_MAX);
float3 min_coord(INT_MAX);
BoundBox *bb = BKE_mesh_boundbox_get(object);
for (const float(&vertex)[3] : bb->vec) {
for (int axis = 0; axis < 3; axis++) {
max_coord[axis] = max_ff(max_coord[axis], vertex[axis]);
min_coord[axis] = min_ff(min_coord[axis], vertex[axis]);
}
}
const float max_diff = max_fff(
max_coord[0] - min_coord[0], max_coord[1] - min_coord[1], max_coord[2] - min_coord[2]);
float scale = 1.0f;
while (import_params.clamp_size < max_diff * scale) {
scale = scale / 10;
}
copy_v3_fl(object->scale, scale);
}
}
} // namespace blender::io::obj

View File

@ -0,0 +1,35 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "BLI_math_vec_types.hh"
#include "BLI_span.hh"
#include "BLI_vector.hh"
struct Object;
struct OBJImportParams;
namespace blender::io::obj {
/**
* Given an invalid polygon (with holes or duplicated vertex indices),
* turn it into possibly multiple polygons that are valid.
*
* \param vertex_coords Polygon's vertex coordinate list.
* \param face_vertex_indices A polygon's indices that index into the given vertex coordinate list.
* \return List of polygons with each element containing indices of one polygon.
* The indices are into face_vertex_indices array.
*/
Vector<Vector<int>> fixup_invalid_polygon(Span<float3> vertex_coords,
Span<int> face_vertex_indices);
/**
* Apply axes transform to the Object, and clamp object dimensions to the specified value.
*/
void transform_object(Object *object, const OBJImportParams &import_params);
} // namespace blender::io::obj

View File

@ -0,0 +1,639 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include "BLI_map.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "parser_string_utils.hh"
#include "obj_import_file_reader.hh"
namespace blender::io::obj {
using std::string;
/**
* Based on the properties of the given Geometry instance, create a new Geometry instance
* or return the previous one.
*
* Also update index offsets which should always happen if a new Geometry instance is created.
*/
static Geometry *create_geometry(Geometry *const prev_geometry,
const eGeometryType new_type,
StringRef name,
const GlobalVertices &global_vertices,
Vector<std::unique_ptr<Geometry>> &r_all_geometries,
VertexIndexOffset &r_offset)
{
auto new_geometry = [&]() {
r_all_geometries.append(std::make_unique<Geometry>());
Geometry *g = r_all_geometries.last().get();
g->geom_type_ = new_type;
g->geometry_name_ = name.is_empty() ? "New object" : name;
r_offset.set_index_offset(global_vertices.vertices.size());
return g;
};
if (prev_geometry && prev_geometry->geom_type_ == GEOM_MESH) {
/* After the creation of a Geometry instance, at least one element has been found in the OBJ
* file that indicates that it is a mesh (basically anything but the vertex positions). */
if (!prev_geometry->face_elements_.is_empty() || prev_geometry->has_vertex_normals_ ||
!prev_geometry->edges_.is_empty()) {
return new_geometry();
}
if (new_type == GEOM_MESH) {
/* A Geometry created initially with a default name now found its name. */
prev_geometry->geometry_name_ = name;
return prev_geometry;
}
if (new_type == GEOM_CURVE) {
/* The object originally created is not a mesh now that curve data
* follows the vertex coordinates list. */
prev_geometry->geom_type_ = GEOM_CURVE;
return prev_geometry;
}
}
if (prev_geometry && prev_geometry->geom_type_ == GEOM_CURVE) {
return new_geometry();
}
return new_geometry();
}
static void geom_add_vertex(Geometry *geom,
const StringRef rest_line,
GlobalVertices &r_global_vertices)
{
float3 curr_vert;
Vector<StringRef> str_vert_split;
split_by_char(rest_line, ' ', str_vert_split);
copy_string_to_float(str_vert_split, FLT_MAX, {curr_vert, 3});
r_global_vertices.vertices.append(curr_vert);
geom->vertex_indices_.append(r_global_vertices.vertices.size() - 1);
}
static void geom_add_vertex_normal(Geometry *geom,
const StringRef rest_line,
GlobalVertices &r_global_vertices)
{
float3 curr_vert_normal;
Vector<StringRef> str_vert_normal_split;
split_by_char(rest_line, ' ', str_vert_normal_split);
copy_string_to_float(str_vert_normal_split, FLT_MAX, {curr_vert_normal, 3});
r_global_vertices.vertex_normals.append(curr_vert_normal);
geom->has_vertex_normals_ = true;
}
static void geom_add_uv_vertex(const StringRef rest_line, GlobalVertices &r_global_vertices)
{
float2 curr_uv_vert;
Vector<StringRef> str_uv_vert_split;
split_by_char(rest_line, ' ', str_uv_vert_split);
copy_string_to_float(str_uv_vert_split, FLT_MAX, {curr_uv_vert, 2});
r_global_vertices.uv_vertices.append(curr_uv_vert);
}
static void geom_add_edge(Geometry *geom,
const StringRef rest_line,
const VertexIndexOffset &offsets,
GlobalVertices &r_global_vertices)
{
int edge_v1 = -1, edge_v2 = -1;
Vector<StringRef> str_edge_split;
split_by_char(rest_line, ' ', str_edge_split);
copy_string_to_int(str_edge_split[0], -1, edge_v1);
copy_string_to_int(str_edge_split[1], -1, edge_v2);
/* Always keep stored indices non-negative and zero-based. */
edge_v1 += edge_v1 < 0 ? r_global_vertices.vertices.size() : -offsets.get_index_offset() - 1;
edge_v2 += edge_v2 < 0 ? r_global_vertices.vertices.size() : -offsets.get_index_offset() - 1;
BLI_assert(edge_v1 >= 0 && edge_v2 >= 0);
geom->edges_.append({static_cast<uint>(edge_v1), static_cast<uint>(edge_v2)});
}
static void geom_add_polygon(Geometry *geom,
const StringRef rest_line,
const GlobalVertices &global_vertices,
const VertexIndexOffset &offsets,
const StringRef state_material_name,
const StringRef state_object_group,
const bool state_shaded_smooth)
{
PolyElem curr_face;
curr_face.shaded_smooth = state_shaded_smooth;
if (!state_material_name.is_empty()) {
curr_face.material_name = state_material_name;
}
if (!state_object_group.is_empty()) {
curr_face.vertex_group = state_object_group;
/* Yes it repeats several times, but another if-check will not reduce steps either. */
geom->use_vertex_groups_ = true;
}
bool face_valid = true;
Vector<StringRef> str_corners_split;
split_by_char(rest_line, ' ', str_corners_split);
for (StringRef str_corner : str_corners_split) {
PolyCorner corner;
const size_t n_slash = std::count(str_corner.begin(), str_corner.end(), '/');
bool got_uv = false, got_normal = false;
if (n_slash == 0) {
/* Case: "f v1 v2 v3". */
copy_string_to_int(str_corner, INT32_MAX, corner.vert_index);
}
else if (n_slash == 1) {
/* Case: "f v1/vt1 v2/vt2 v3/vt3". */
Vector<StringRef> vert_uv_split;
split_by_char(str_corner, '/', vert_uv_split);
if (vert_uv_split.size() != 1 && vert_uv_split.size() != 2) {
fprintf(stderr, "Invalid face syntax '%s', ignoring\n", std::string(str_corner).c_str());
face_valid = false;
}
else {
copy_string_to_int(vert_uv_split[0], INT32_MAX, corner.vert_index);
if (vert_uv_split.size() == 2) {
copy_string_to_int(vert_uv_split[1], INT32_MAX, corner.uv_vert_index);
got_uv = corner.uv_vert_index != INT32_MAX;
}
}
}
else if (n_slash == 2) {
/* Case: "f v1//vn1 v2//vn2 v3//vn3". */
/* Case: "f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3". */
Vector<StringRef> vert_uv_normal_split;
split_by_char(str_corner, '/', vert_uv_normal_split);
if (vert_uv_normal_split.size() != 2 && vert_uv_normal_split.size() != 3) {
fprintf(stderr, "Invalid face syntax '%s', ignoring\n", std::string(str_corner).c_str());
face_valid = false;
}
else {
copy_string_to_int(vert_uv_normal_split[0], INT32_MAX, corner.vert_index);
if (vert_uv_normal_split.size() == 3) {
copy_string_to_int(vert_uv_normal_split[1], INT32_MAX, corner.uv_vert_index);
got_uv = corner.uv_vert_index != INT32_MAX;
copy_string_to_int(vert_uv_normal_split[2], INT32_MAX, corner.vertex_normal_index);
got_normal = corner.vertex_normal_index != INT32_MAX;
}
else {
copy_string_to_int(vert_uv_normal_split[1], INT32_MAX, corner.vertex_normal_index);
got_normal = corner.vertex_normal_index != INT32_MAX;
}
}
}
/* Always keep stored indices non-negative and zero-based. */
corner.vert_index += corner.vert_index < 0 ? global_vertices.vertices.size() :
-offsets.get_index_offset() - 1;
if (corner.vert_index < 0 || corner.vert_index >= global_vertices.vertices.size()) {
fprintf(stderr,
"Invalid vertex index %i (valid range [0, %zi)), ignoring face\n",
corner.vert_index,
global_vertices.vertices.size());
face_valid = false;
}
if (got_uv) {
corner.uv_vert_index += corner.uv_vert_index < 0 ? global_vertices.uv_vertices.size() : -1;
if (corner.uv_vert_index < 0 || corner.uv_vert_index >= global_vertices.uv_vertices.size()) {
fprintf(stderr,
"Invalid UV index %i (valid range [0, %zi)), ignoring face\n",
corner.uv_vert_index,
global_vertices.uv_vertices.size());
face_valid = false;
}
}
if (got_normal) {
corner.vertex_normal_index += corner.vertex_normal_index < 0 ?
global_vertices.vertex_normals.size() :
-1;
if (corner.vertex_normal_index < 0 ||
corner.vertex_normal_index >= global_vertices.vertex_normals.size()) {
fprintf(stderr,
"Invalid normal index %i (valid range [0, %zi)), ignoring face\n",
corner.vertex_normal_index,
global_vertices.vertex_normals.size());
face_valid = false;
}
}
curr_face.face_corners.append(corner);
}
if (face_valid) {
geom->face_elements_.append(curr_face);
geom->total_loops_ += curr_face.face_corners.size();
}
}
static Geometry *geom_set_curve_type(Geometry *geom,
const StringRef rest_line,
const GlobalVertices &global_vertices,
const StringRef state_object_group,
VertexIndexOffset &r_offsets,
Vector<std::unique_ptr<Geometry>> &r_all_geometries)
{
if (rest_line.find("bspline") == StringRef::not_found) {
std::cerr << "Curve type not supported:'" << rest_line << "'" << std::endl;
return geom;
}
geom = create_geometry(
geom, GEOM_CURVE, state_object_group, global_vertices, r_all_geometries, r_offsets);
geom->nurbs_element_.group_ = state_object_group;
return geom;
}
static void geom_set_curve_degree(Geometry *geom, const StringRef rest_line)
{
copy_string_to_int(rest_line, 3, geom->nurbs_element_.degree);
}
static void geom_add_curve_vertex_indices(Geometry *geom,
const StringRef rest_line,
const GlobalVertices &global_vertices)
{
Vector<StringRef> str_curv_split;
split_by_char(rest_line, ' ', str_curv_split);
/* Remove "0.0" and "1.0" from the strings. They are hardcoded. */
str_curv_split.remove(0);
str_curv_split.remove(0);
geom->nurbs_element_.curv_indices.resize(str_curv_split.size());
copy_string_to_int(str_curv_split, INT32_MAX, geom->nurbs_element_.curv_indices);
for (int &curv_index : geom->nurbs_element_.curv_indices) {
/* Always keep stored indices non-negative and zero-based. */
curv_index += curv_index < 0 ? global_vertices.vertices.size() : -1;
}
}
static void geom_add_curve_parameters(Geometry *geom, const StringRef rest_line)
{
Vector<StringRef> str_parm_split;
split_by_char(rest_line, ' ', str_parm_split);
if (str_parm_split[0] != "u" && str_parm_split[0] != "v") {
std::cerr << "Surfaces are not supported:'" << str_parm_split[0] << "'" << std::endl;
return;
}
str_parm_split.remove(0);
geom->nurbs_element_.parm.resize(str_parm_split.size());
copy_string_to_float(str_parm_split, FLT_MAX, geom->nurbs_element_.parm);
}
static void geom_update_object_group(const StringRef rest_line, std::string &r_state_object_group)
{
if (rest_line.find("off") != string::npos || rest_line.find("null") != string::npos ||
rest_line.find("default") != string::npos) {
/* Set group for future elements like faces or curves to empty. */
r_state_object_group = "";
return;
}
r_state_object_group = rest_line;
}
static void geom_update_polygon_material(Geometry *geom,
const StringRef rest_line,
std::string &r_state_material_name)
{
/* Materials may repeat if faces are written without sorting. */
geom->material_names_.add(string(rest_line));
r_state_material_name = rest_line;
}
static void geom_update_smooth_group(const StringRef rest_line, bool &r_state_shaded_smooth)
{
/* Some implementations use "0" and "null" too, in addition to "off". */
if (rest_line != "0" && rest_line.find("off") == StringRef::not_found &&
rest_line.find("null") == StringRef::not_found) {
int smooth = 0;
copy_string_to_int(rest_line, 0, smooth);
r_state_shaded_smooth = smooth != 0;
}
else {
/* The OBJ file explicitly set shading to off. */
r_state_shaded_smooth = false;
}
}
/**
* Open OBJ file at the path given in import parameters.
*/
OBJParser::OBJParser(const OBJImportParams &import_params) : import_params_(import_params)
{
obj_file_.open(import_params_.filepath);
if (!obj_file_.good()) {
fprintf(stderr, "Cannot read from OBJ file:'%s'.\n", import_params_.filepath);
return;
}
}
/**
* Read the OBJ file line by line and create OBJ Geometry instances. Also store all the vertex
* and UV vertex coordinates in a struct accessible by all objects.
*/
void OBJParser::parse(Vector<std::unique_ptr<Geometry>> &r_all_geometries,
GlobalVertices &r_global_vertices)
{
if (!obj_file_.good()) {
return;
}
string line;
/* Store vertex coordinates that belong to other Geometry instances. */
VertexIndexOffset offsets;
/* Non owning raw pointer to a Geometry. To be updated while creating a new Geometry. */
Geometry *curr_geom = create_geometry(
nullptr, GEOM_MESH, "", r_global_vertices, r_all_geometries, offsets);
/* State-setting variables: if set, they remain the same for the remaining
* elements in the object. */
bool state_shaded_smooth = false;
string state_object_group;
string state_material_name;
while (std::getline(obj_file_, line)) {
/* Keep reading new lines if the last character is `\`. */
/* Another way is to make a getline wrapper and use it in the while condition. */
read_next_line(obj_file_, line);
StringRef line_key, rest_line;
split_line_key_rest(line, line_key, rest_line);
if (line.empty() || rest_line.is_empty()) {
continue;
}
switch (line_key_str_to_enum(line_key)) {
case eOBJLineKey::V: {
geom_add_vertex(curr_geom, rest_line, r_global_vertices);
break;
}
case eOBJLineKey::VN: {
geom_add_vertex_normal(curr_geom, rest_line, r_global_vertices);
break;
}
case eOBJLineKey::VT: {
geom_add_uv_vertex(rest_line, r_global_vertices);
break;
}
case eOBJLineKey::F: {
geom_add_polygon(curr_geom,
rest_line,
r_global_vertices,
offsets,
state_material_name,
state_material_name,
state_shaded_smooth);
break;
}
case eOBJLineKey::L: {
geom_add_edge(curr_geom, rest_line, offsets, r_global_vertices);
break;
}
case eOBJLineKey::CSTYPE: {
curr_geom = geom_set_curve_type(curr_geom,
rest_line,
r_global_vertices,
state_object_group,
offsets,
r_all_geometries);
break;
}
case eOBJLineKey::DEG: {
geom_set_curve_degree(curr_geom, rest_line);
break;
}
case eOBJLineKey::CURV: {
geom_add_curve_vertex_indices(curr_geom, rest_line, r_global_vertices);
break;
}
case eOBJLineKey::PARM: {
geom_add_curve_parameters(curr_geom, rest_line);
break;
}
case eOBJLineKey::O: {
state_shaded_smooth = false;
state_object_group = "";
state_material_name = "";
curr_geom = create_geometry(
curr_geom, GEOM_MESH, rest_line, r_global_vertices, r_all_geometries, offsets);
break;
}
case eOBJLineKey::G: {
geom_update_object_group(rest_line, state_object_group);
break;
}
case eOBJLineKey::S: {
geom_update_smooth_group(rest_line, state_shaded_smooth);
break;
}
case eOBJLineKey::USEMTL: {
geom_update_polygon_material(curr_geom, rest_line, state_material_name);
break;
}
case eOBJLineKey::MTLLIB: {
mtl_libraries_.append(string(rest_line));
break;
}
case eOBJLineKey::COMMENT:
break;
default:
std::cout << "Element not recognised: '" << line_key << "'" << std::endl;
break;
}
}
}
/**
* Skip all texture map options and get the filepath from a "map_" line.
*/
static StringRef skip_unsupported_options(StringRef line)
{
TextureMapOptions map_options;
StringRef last_option;
int64_t last_option_pos = 0;
/* Find the last texture map option. */
for (StringRef option : map_options.all_options()) {
const int64_t pos{line.find(option)};
/* Equality (>=) takes care of finding an option in the beginning of the line. Avoid messing
* with signed-unsigned int comparison. */
if (pos != StringRef::not_found && pos >= last_option_pos) {
last_option = option;
last_option_pos = pos;
}
}
if (last_option.is_empty()) {
/* No option found, line is the filepath */
return line;
}
/* Remove upto start of the last option + size of the last option + space after it. */
line = line.drop_prefix(last_option_pos + last_option.size() + 1);
for (int i = 0; i < map_options.number_of_args(last_option); i++) {
const int64_t pos_space{line.find_first_of(' ')};
if (pos_space != StringRef::not_found) {
BLI_assert(pos_space + 1 < line.size());
line = line.drop_prefix(pos_space + 1);
}
}
return line;
}
/**
* Fix incoming texture map line keys for variations due to other exporters.
*/
static string fix_bad_map_keys(StringRef map_key)
{
string new_map_key(map_key);
if (map_key == "refl") {
new_map_key = "map_refl";
}
if (map_key.find("bump") != StringRef::not_found) {
/* Handles both "bump" and "map_Bump" */
new_map_key = "map_Bump";
}
return new_map_key;
}
/**
* Return a list of all material library filepaths referenced by the OBJ file.
*/
Span<std::string> OBJParser::mtl_libraries() const
{
return mtl_libraries_;
}
/**
* Open material library file.
*/
MTLParser::MTLParser(StringRef mtl_library, StringRefNull obj_filepath)
{
char obj_file_dir[FILE_MAXDIR];
BLI_split_dir_part(obj_filepath.data(), obj_file_dir, FILE_MAXDIR);
BLI_path_join(mtl_file_path_, FILE_MAX, obj_file_dir, mtl_library.data(), NULL);
BLI_split_dir_part(mtl_file_path_, mtl_dir_path_, FILE_MAXDIR);
mtl_file_.open(mtl_file_path_);
if (!mtl_file_.good()) {
fprintf(stderr, "Cannot read from MTL file:'%s'\n", mtl_file_path_);
return;
}
}
/**
* Read MTL file(s) and add MTLMaterial instances to the given Map reference.
*/
void MTLParser::parse_and_store(Map<string, std::unique_ptr<MTLMaterial>> &r_mtl_materials)
{
if (!mtl_file_.good()) {
return;
}
string line;
MTLMaterial *current_mtlmaterial = nullptr;
while (std::getline(mtl_file_, line)) {
StringRef line_key, rest_line;
split_line_key_rest(line, line_key, rest_line);
if (line.empty() || rest_line.is_empty()) {
continue;
}
/* Fix lower case/ incomplete texture map identifiers. */
const string fixed_key = fix_bad_map_keys(line_key);
line_key = fixed_key;
if (line_key == "newmtl") {
if (r_mtl_materials.remove_as(rest_line)) {
std::cerr << "Duplicate material found:'" << rest_line
<< "', using the last encountered Material definition." << std::endl;
}
current_mtlmaterial =
r_mtl_materials.lookup_or_add(string(rest_line), std::make_unique<MTLMaterial>()).get();
}
else if (line_key == "Ns") {
copy_string_to_float(rest_line, 324.0f, current_mtlmaterial->Ns);
}
else if (line_key == "Ka") {
Vector<StringRef> str_ka_split;
split_by_char(rest_line, ' ', str_ka_split);
copy_string_to_float(str_ka_split, 0.0f, {current_mtlmaterial->Ka, 3});
}
else if (line_key == "Kd") {
Vector<StringRef> str_kd_split;
split_by_char(rest_line, ' ', str_kd_split);
copy_string_to_float(str_kd_split, 0.8f, {current_mtlmaterial->Kd, 3});
}
else if (line_key == "Ks") {
Vector<StringRef> str_ks_split;
split_by_char(rest_line, ' ', str_ks_split);
copy_string_to_float(str_ks_split, 0.5f, {current_mtlmaterial->Ks, 3});
}
else if (line_key == "Ke") {
Vector<StringRef> str_ke_split;
split_by_char(rest_line, ' ', str_ke_split);
copy_string_to_float(str_ke_split, 0.0f, {current_mtlmaterial->Ke, 3});
}
else if (line_key == "Ni") {
copy_string_to_float(rest_line, 1.45f, current_mtlmaterial->Ni);
}
else if (line_key == "d") {
copy_string_to_float(rest_line, 1.0f, current_mtlmaterial->d);
}
else if (line_key == "illum") {
copy_string_to_int(rest_line, 2, current_mtlmaterial->illum);
}
/* Parse image textures. */
else if (line_key.find("map_") != StringRef::not_found) {
/* TODO howardt: fix this */
eMTLSyntaxElement line_key_enum = mtl_line_key_str_to_enum(line_key);
if (line_key_enum == eMTLSyntaxElement::string ||
!current_mtlmaterial->texture_maps.contains_as(line_key_enum)) {
/* No supported texture map found. */
std::cerr << "Texture map type not supported:'" << line_key << "'" << std::endl;
continue;
}
tex_map_XX &tex_map = current_mtlmaterial->texture_maps.lookup(line_key_enum);
Vector<StringRef> str_map_xx_split;
split_by_char(rest_line, ' ', str_map_xx_split);
/* TODO ankitm: use `skip_unsupported_options` for parsing these options too? */
const int64_t pos_o{str_map_xx_split.first_index_of_try("-o")};
if (pos_o != -1 && pos_o + 3 < str_map_xx_split.size()) {
copy_string_to_float({str_map_xx_split[pos_o + 1],
str_map_xx_split[pos_o + 2],
str_map_xx_split[pos_o + 3]},
0.0f,
{tex_map.translation, 3});
}
const int64_t pos_s{str_map_xx_split.first_index_of_try("-s")};
if (pos_s != -1 && pos_s + 3 < str_map_xx_split.size()) {
copy_string_to_float({str_map_xx_split[pos_s + 1],
str_map_xx_split[pos_s + 2],
str_map_xx_split[pos_s + 3]},
1.0f,
{tex_map.scale, 3});
}
/* Only specific to Normal Map node. */
const int64_t pos_bm{str_map_xx_split.first_index_of_try("-bm")};
if (pos_bm != -1 && pos_bm + 1 < str_map_xx_split.size()) {
copy_string_to_float(
str_map_xx_split[pos_bm + 1], 0.0f, current_mtlmaterial->map_Bump_strength);
}
const int64_t pos_projection{str_map_xx_split.first_index_of_try("-type")};
if (pos_projection != -1 && pos_projection + 1 < str_map_xx_split.size()) {
/* Only Sphere is supported, so whatever the type is, set it to Sphere. */
tex_map.projection_type = SHD_PROJ_SPHERE;
if (str_map_xx_split[pos_projection + 1] != "sphere") {
std::cerr << "Using projection type 'sphere', not:'"
<< str_map_xx_split[pos_projection + 1] << "'." << std::endl;
}
}
/* Skip all unsupported options and arguments. */
tex_map.image_path = string(skip_unsupported_options(rest_line));
tex_map.mtl_dir_path = mtl_dir_path_;
}
}
}
} // namespace blender::io::obj

View File

@ -0,0 +1,151 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "BLI_fileops.hh"
#include "IO_wavefront_obj.h"
#include "obj_import_mtl.hh"
#include "obj_import_objects.hh"
namespace blender::io::obj {
/* Note: the OBJ parser implementation is planned to get fairly large changes "soon",
* so don't read too much into current implementation... */
class OBJParser {
private:
const OBJImportParams &import_params_;
blender::fstream obj_file_;
Vector<std::string> mtl_libraries_;
public:
OBJParser(const OBJImportParams &import_params);
void parse(Vector<std::unique_ptr<Geometry>> &r_all_geometries,
GlobalVertices &r_global_vertices);
Span<std::string> mtl_libraries() const;
};
enum class eOBJLineKey {
V,
VN,
VT,
F,
L,
CSTYPE,
DEG,
CURV,
PARM,
O,
G,
S,
USEMTL,
MTLLIB,
COMMENT
};
constexpr eOBJLineKey line_key_str_to_enum(const std::string_view key_str)
{
if (key_str == "v" || key_str == "V") {
return eOBJLineKey::V;
}
if (key_str == "vn" || key_str == "VN") {
return eOBJLineKey::VN;
}
if (key_str == "vt" || key_str == "VT") {
return eOBJLineKey::VT;
}
if (key_str == "f" || key_str == "F") {
return eOBJLineKey::F;
}
if (key_str == "l" || key_str == "L") {
return eOBJLineKey::L;
}
if (key_str == "cstype" || key_str == "CSTYPE") {
return eOBJLineKey::CSTYPE;
}
if (key_str == "deg" || key_str == "DEG") {
return eOBJLineKey::DEG;
}
if (key_str == "curv" || key_str == "CURV") {
return eOBJLineKey::CURV;
}
if (key_str == "parm" || key_str == "PARM") {
return eOBJLineKey::PARM;
}
if (key_str == "o" || key_str == "O") {
return eOBJLineKey::O;
}
if (key_str == "g" || key_str == "G") {
return eOBJLineKey::G;
}
if (key_str == "s" || key_str == "S") {
return eOBJLineKey::S;
}
if (key_str == "usemtl" || key_str == "USEMTL") {
return eOBJLineKey::USEMTL;
}
if (key_str == "mtllib" || key_str == "MTLLIB") {
return eOBJLineKey::MTLLIB;
}
if (key_str == "#") {
return eOBJLineKey::COMMENT;
}
return eOBJLineKey::COMMENT;
}
/**
* All texture map options with number of arguments they accept.
*/
class TextureMapOptions {
private:
Map<const std::string, int> tex_map_options;
public:
TextureMapOptions()
{
tex_map_options.add_new("-blendu", 1);
tex_map_options.add_new("-blendv", 1);
tex_map_options.add_new("-boost", 1);
tex_map_options.add_new("-mm", 2);
tex_map_options.add_new("-o", 3);
tex_map_options.add_new("-s", 3);
tex_map_options.add_new("-t", 3);
tex_map_options.add_new("-texres", 1);
tex_map_options.add_new("-clamp", 1);
tex_map_options.add_new("-bm", 1);
tex_map_options.add_new("-imfchan", 1);
}
/**
* All valid option strings.
*/
Map<const std::string, int>::KeyIterator all_options() const
{
return tex_map_options.keys();
}
int number_of_args(StringRef option) const
{
return tex_map_options.lookup_as(std::string(option));
}
};
class MTLParser {
private:
char mtl_file_path_[FILE_MAX];
/**
* Directory in which the MTL file is found.
*/
char mtl_dir_path_[FILE_MAX];
blender::fstream mtl_file_;
public:
MTLParser(StringRef mtl_library_, StringRefNull obj_filepath);
void parse_and_store(Map<std::string, std::unique_ptr<MTLMaterial>> &r_mtl_materials);
};
} // namespace blender::io::obj

View File

@ -0,0 +1,380 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include "DNA_material_types.h"
#include "DNA_mesh_types.h"
#include "DNA_scene_types.h"
#include "BKE_customdata.h"
#include "BKE_material.h"
#include "BKE_mesh.h"
#include "BKE_node_tree_update.h"
#include "BKE_object.h"
#include "BKE_object_deform.h"
#include "BLI_math_vector.h"
#include "BLI_set.hh"
#include "importer_mesh_utils.hh"
#include "obj_import_mesh.hh"
namespace blender::io::obj {
Object *MeshFromGeometry::create_mesh(
Main *bmain,
const Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
Map<std::string, Material *> &created_materials,
const OBJImportParams &import_params)
{
std::string ob_name{mesh_geometry_.geometry_name_};
if (ob_name.empty()) {
ob_name = "Untitled";
}
fixup_invalid_faces();
const int64_t tot_verts_object{mesh_geometry_.vertex_indices_.size()};
/* Total explicitly imported edges, not the ones belonging the polygons to be created. */
const int64_t tot_edges{mesh_geometry_.edges_.size()};
const int64_t tot_face_elems{mesh_geometry_.face_elements_.size()};
const int64_t tot_loops{mesh_geometry_.total_loops_};
Mesh *mesh = BKE_mesh_new_nomain(tot_verts_object, tot_edges, 0, tot_loops, tot_face_elems);
Object *obj = BKE_object_add_only_object(bmain, OB_MESH, ob_name.c_str());
obj->data = BKE_object_obdata_add_from_type(bmain, OB_MESH, ob_name.c_str());
create_vertices(mesh);
create_polys_loops(obj, mesh);
create_edges(mesh);
create_uv_verts(mesh);
create_normals(mesh);
create_materials(bmain, materials, created_materials, obj);
bool verbose_validate = false;
#ifdef DEBUG
verbose_validate = true;
#endif
BKE_mesh_validate(mesh, verbose_validate, false);
transform_object(obj, import_params);
/* FIXME: after 2.80; `mesh->flag` isn't copied by #BKE_mesh_nomain_to_mesh() */
const short autosmooth = (mesh->flag & ME_AUTOSMOOTH);
Mesh *dst = static_cast<Mesh *>(obj->data);
BKE_mesh_nomain_to_mesh(mesh, dst, obj, &CD_MASK_EVERYTHING, true);
dst->flag |= autosmooth;
return obj;
}
/**
* OBJ files coming from the wild might have faces that are invalid in Blender
* (mostly with duplicate vertex indices, used by some software to indicate
* polygons with holes). This method tries to fix them up.
*/
void MeshFromGeometry::fixup_invalid_faces()
{
for (int64_t face_idx = 0; face_idx < mesh_geometry_.face_elements_.size(); ++face_idx) {
const PolyElem &curr_face = mesh_geometry_.face_elements_[face_idx];
if (curr_face.face_corners.size() < 3) {
/* Skip and remove faces that have fewer than 3 corners. */
mesh_geometry_.total_loops_ -= curr_face.face_corners.size();
mesh_geometry_.face_elements_.remove_and_reorder(face_idx);
continue;
}
/* Check if face is invalid for Blender conventions:
* basically whether it has duplicate vertex indices. */
bool valid = true;
Set<int, 8> used_verts;
for (const PolyCorner &corner : curr_face.face_corners) {
if (used_verts.contains(corner.vert_index)) {
valid = false;
break;
}
used_verts.add(corner.vert_index);
}
if (valid) {
continue;
}
/* We have an invalid face, have to turn it into possibly
* multiple valid faces. */
Vector<int, 8> face_verts;
Vector<int, 8> face_uvs;
Vector<int, 8> face_normals;
face_verts.reserve(curr_face.face_corners.size());
face_uvs.reserve(curr_face.face_corners.size());
face_normals.reserve(curr_face.face_corners.size());
for (const PolyCorner &corner : curr_face.face_corners) {
face_verts.append(corner.vert_index);
face_normals.append(corner.vertex_normal_index);
face_uvs.append(corner.uv_vert_index);
}
std::string face_vertex_group = curr_face.vertex_group;
std::string face_material_name = curr_face.material_name;
bool face_shaded_smooth = curr_face.shaded_smooth;
/* Remove the invalid face. */
mesh_geometry_.total_loops_ -= curr_face.face_corners.size();
mesh_geometry_.face_elements_.remove_and_reorder(face_idx);
Vector<Vector<int>> new_faces = fixup_invalid_polygon(global_vertices_.vertices, face_verts);
/* Create the newly formed faces. */
for (Span<int> face : new_faces) {
if (face.size() < 3) {
continue;
}
PolyElem new_face{};
new_face.vertex_group = face_vertex_group;
new_face.material_name = face_material_name;
new_face.shaded_smooth = face_shaded_smooth;
new_face.face_corners.reserve(face.size());
for (int idx : face) {
BLI_assert(idx >= 0 && idx < face_verts.size());
new_face.face_corners.append({face_verts[idx], face_uvs[idx], face_normals[idx]});
}
mesh_geometry_.face_elements_.append(new_face);
mesh_geometry_.total_loops_ += face.size();
}
}
}
void MeshFromGeometry::create_vertices(Mesh *mesh)
{
const int64_t tot_verts_object{mesh_geometry_.vertex_indices_.size()};
for (int i = 0; i < tot_verts_object; ++i) {
if (mesh_geometry_.vertex_indices_[i] < global_vertices_.vertices.size()) {
copy_v3_v3(mesh->mvert[i].co, global_vertices_.vertices[mesh_geometry_.vertex_indices_[i]]);
}
else {
std::cerr << "Vertex index:" << mesh_geometry_.vertex_indices_[i]
<< " larger than total vertices:" << global_vertices_.vertices.size() << " ."
<< std::endl;
}
}
}
/**
* Create polygons for the Mesh, set smooth shading flag, deform group name, assigned material
* also.
*
* It must receive all polygons to be added to the mesh. Remove holes from polygons before
* calling this.
*/
void MeshFromGeometry::create_polys_loops(Object *obj, Mesh *mesh)
{
/* Will not be used if vertex groups are not imported. */
mesh->dvert = nullptr;
float weight = 0.0f;
const int64_t total_verts = mesh_geometry_.vertex_indices_.size();
if (total_verts && mesh_geometry_.use_vertex_groups_) {
mesh->dvert = static_cast<MDeformVert *>(
CustomData_add_layer(&mesh->vdata, CD_MDEFORMVERT, CD_CALLOC, nullptr, total_verts));
weight = 1.0f / total_verts;
}
else {
UNUSED_VARS(weight);
}
/* Do not remove elements from the VectorSet since order of insertion is required.
* StringRef is fine since per-face deform group name outlives the VectorSet. */
VectorSet<StringRef> group_names;
const int64_t tot_face_elems{mesh->totpoly};
int tot_loop_idx = 0;
for (int poly_idx = 0; poly_idx < tot_face_elems; ++poly_idx) {
const PolyElem &curr_face = mesh_geometry_.face_elements_[poly_idx];
if (curr_face.face_corners.size() < 3) {
/* Don't add single vertex face, or edges. */
std::cerr << "Face with less than 3 vertices found, skipping." << std::endl;
continue;
}
MPoly &mpoly = mesh->mpoly[poly_idx];
mpoly.totloop = curr_face.face_corners.size();
mpoly.loopstart = tot_loop_idx;
if (curr_face.shaded_smooth) {
mpoly.flag |= ME_SMOOTH;
}
mpoly.mat_nr = mesh_geometry_.material_names_.index_of_try(curr_face.material_name);
/* Importing obj files without any materials would result in negative indices, which is not
* supported. */
if (mpoly.mat_nr < 0) {
mpoly.mat_nr = 0;
}
for (const PolyCorner &curr_corner : curr_face.face_corners) {
MLoop &mloop = mesh->mloop[tot_loop_idx];
tot_loop_idx++;
mloop.v = curr_corner.vert_index;
if (!mesh->dvert) {
continue;
}
/* Iterating over mloop results in finding the same vertex multiple times.
* Another way is to allocate memory for dvert while creating vertices and fill them here.
*/
MDeformVert &def_vert = mesh->dvert[mloop.v];
if (!def_vert.dw) {
def_vert.dw = static_cast<MDeformWeight *>(
MEM_callocN(sizeof(MDeformWeight), "OBJ Import Deform Weight"));
}
/* Every vertex in a face is assigned the same deform group. */
int64_t pos_name{group_names.index_of_try(curr_face.vertex_group)};
if (pos_name == -1) {
group_names.add_new(curr_face.vertex_group);
pos_name = group_names.size() - 1;
}
BLI_assert(pos_name >= 0);
/* Deform group number (def_nr) must behave like an index into the names' list. */
*(def_vert.dw) = {static_cast<unsigned int>(pos_name), weight};
}
}
if (!mesh->dvert) {
return;
}
/* Add deform group(s) to the object's defbase. */
for (StringRef name : group_names) {
/* Adding groups in this order assumes that def_nr is an index into the names' list. */
BKE_object_defgroup_add_name(obj, name.data());
}
}
/**
* Add explicitly imported OBJ edges to the mesh.
*/
void MeshFromGeometry::create_edges(Mesh *mesh)
{
const int64_t tot_edges{mesh_geometry_.edges_.size()};
const int64_t total_verts{mesh_geometry_.vertex_indices_.size()};
UNUSED_VARS_NDEBUG(total_verts);
for (int i = 0; i < tot_edges; ++i) {
const MEdge &src_edge = mesh_geometry_.edges_[i];
MEdge &dst_edge = mesh->medge[i];
BLI_assert(src_edge.v1 < total_verts && src_edge.v2 < total_verts);
dst_edge.v1 = src_edge.v1;
dst_edge.v2 = src_edge.v2;
dst_edge.flag = ME_LOOSEEDGE;
}
/* Set argument `update` to true so that existing, explicitly imported edges can be merged
* with the new ones created from polygons. */
BKE_mesh_calc_edges(mesh, true, false);
BKE_mesh_calc_edges_loose(mesh);
}
/**
* Add UV layer and vertices to the Mesh.
*/
void MeshFromGeometry::create_uv_verts(Mesh *mesh)
{
if (global_vertices_.uv_vertices.size() <= 0) {
return;
}
MLoopUV *mluv_dst = static_cast<MLoopUV *>(CustomData_add_layer(
&mesh->ldata, CD_MLOOPUV, CD_DEFAULT, nullptr, mesh_geometry_.total_loops_));
int tot_loop_idx = 0;
for (const PolyElem &curr_face : mesh_geometry_.face_elements_) {
for (const PolyCorner &curr_corner : curr_face.face_corners) {
if (curr_corner.uv_vert_index >= 0 &&
curr_corner.uv_vert_index < global_vertices_.uv_vertices.size()) {
const float2 &mluv_src = global_vertices_.uv_vertices[curr_corner.uv_vert_index];
copy_v2_v2(mluv_dst[tot_loop_idx].uv, mluv_src);
tot_loop_idx++;
}
}
}
}
static Material *get_or_create_material(
Main *bmain,
const std::string &name,
const Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
Map<std::string, Material *> &created_materials)
{
/* Have we created this material already? */
Material **found_mat = created_materials.lookup_ptr(name);
if (found_mat != nullptr) {
return *found_mat;
}
/* We have not, will have to create it. */
if (!materials.contains(name)) {
std::cerr << "Material named '" << name << "' not found in material library." << std::endl;
return nullptr;
}
Material *mat = BKE_material_add(bmain, name.c_str());
const MTLMaterial &mtl = *materials.lookup(name);
ShaderNodetreeWrap mat_wrap{bmain, mtl, mat};
/* Viewport shading uses legacy r,g,b material values. */
if (mtl.Kd[0] >= 0 && mtl.Kd[1] >= 0 && mtl.Kd[2] >= 0) {
mat->r = mtl.Kd[0];
mat->g = mtl.Kd[1];
mat->b = mtl.Kd[2];
}
mat->use_nodes = true;
mat->nodetree = mat_wrap.get_nodetree();
BKE_ntree_update_main_tree(bmain, mat->nodetree, nullptr);
created_materials.add_new(name, mat);
return mat;
}
/**
* Add materials and the nodetree to the Mesh Object.
*/
void MeshFromGeometry::create_materials(
Main *bmain,
const Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
Map<std::string, Material *> &created_materials,
Object *obj)
{
for (const std::string &name : mesh_geometry_.material_names_) {
Material *mat = get_or_create_material(bmain, name, materials, created_materials);
if (mat == nullptr) {
continue;
}
BKE_object_material_slot_add(bmain, obj);
BKE_object_material_assign(bmain, obj, mat, obj->totcol, BKE_MAT_ASSIGN_USERPREF);
}
}
/**
* Needs more clarity about what is expected in the viewport if the function works.
*/
void MeshFromGeometry::create_normals(Mesh *mesh)
{
/* No normal data: nothing to do. */
if (global_vertices_.vertex_normals.is_empty() || !mesh_geometry_.has_vertex_normals_) {
return;
}
float(*loop_normals)[3] = static_cast<float(*)[3]>(
MEM_malloc_arrayN(mesh_geometry_.total_loops_, sizeof(float[3]), __func__));
int tot_loop_idx = 0;
for (const PolyElem &curr_face : mesh_geometry_.face_elements_) {
for (const PolyCorner &curr_corner : curr_face.face_corners) {
int n_index = curr_corner.vertex_normal_index;
float3 normal(0, 0, 0);
if (n_index >= 0) {
normal = global_vertices_.vertex_normals[n_index];
}
copy_v3_v3(loop_normals[tot_loop_idx], normal);
tot_loop_idx++;
}
}
mesh->flag |= ME_AUTOSMOOTH;
BKE_mesh_set_custom_normals(mesh, loop_normals);
MEM_freeN(loop_normals);
}
} // namespace blender::io::obj

View File

@ -0,0 +1,52 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "BKE_lib_id.h"
#include "BLI_utility_mixins.hh"
#include "obj_import_mtl.hh"
#include "obj_import_objects.hh"
struct Material;
namespace blender::io::obj {
/**
* Make a Blender Mesh Object from a Geometry of GEOM_MESH type.
*/
class MeshFromGeometry : NonMovable, NonCopyable {
private:
Geometry &mesh_geometry_;
const GlobalVertices &global_vertices_;
public:
MeshFromGeometry(Geometry &mesh_geometry, const GlobalVertices &global_vertices)
: mesh_geometry_(mesh_geometry), global_vertices_(global_vertices)
{
}
Object *create_mesh(Main *bmain,
const Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
Map<std::string, Material *> &created_materials,
const OBJImportParams &import_params);
private:
void fixup_invalid_faces();
void create_vertices(Mesh *mesh);
void create_polys_loops(Object *obj, Mesh *mesh);
void create_edges(Mesh *mesh);
void create_uv_verts(Mesh *mesh);
void create_materials(Main *bmain,
const Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
Map<std::string, Material *> &created_materials,
Object *obj);
void create_normals(Mesh *mesh);
};
} // namespace blender::io::obj

View File

@ -0,0 +1,386 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include "BKE_image.h"
#include "BKE_node.h"
#include "BLI_map.hh"
#include "BLI_math_vector.h"
#include "DNA_material_types.h"
#include "DNA_node_types.h"
#include "NOD_shader.h"
/* TODO: move eMTLSyntaxElement out of following file into a more neutral place */
#include "obj_export_io.hh"
#include "obj_import_mtl.hh"
#include "parser_string_utils.hh"
namespace blender::io::obj {
/**
* Set the socket's (of given ID) value to the given number(s).
* Only float value(s) can be set using this method.
*/
static void set_property_of_socket(eNodeSocketDatatype property_type,
StringRef socket_id,
Span<float> value,
bNode *r_node)
{
BLI_assert(r_node);
bNodeSocket *socket{nodeFindSocket(r_node, SOCK_IN, socket_id.data())};
BLI_assert(socket && socket->type == property_type);
switch (property_type) {
case SOCK_FLOAT: {
BLI_assert(value.size() == 1);
static_cast<bNodeSocketValueFloat *>(socket->default_value)->value = value[0];
break;
}
case SOCK_RGBA: {
/* Alpha will be added manually. It is not read from the MTL file either. */
BLI_assert(value.size() == 3);
copy_v3_v3(static_cast<bNodeSocketValueRGBA *>(socket->default_value)->value, value.data());
static_cast<bNodeSocketValueRGBA *>(socket->default_value)->value[3] = 1.0f;
break;
}
case SOCK_VECTOR: {
BLI_assert(value.size() == 3);
copy_v4_v4(static_cast<bNodeSocketValueVector *>(socket->default_value)->value,
value.data());
break;
}
default: {
BLI_assert(0);
break;
}
}
}
static bool load_texture_image_at_path(Main *bmain,
const tex_map_XX &tex_map,
bNode *r_node,
const std::string &path)
{
Image *tex_image = BKE_image_load(bmain, path.c_str());
if (!tex_image) {
fprintf(stderr, "Cannot load image file: '%s'\n", path.c_str());
return false;
}
fprintf(stderr, "Loaded image from: '%s'\n", path.c_str());
r_node->id = reinterpret_cast<ID *>(tex_image);
NodeTexImage *image = static_cast<NodeTexImage *>(r_node->storage);
image->projection = tex_map.projection_type;
return true;
}
/**
* Load image for Image Texture node and set the node properties.
* Return success if Image can be loaded successfully.
*/
static bool load_texture_image(Main *bmain, const tex_map_XX &tex_map, bNode *r_node)
{
BLI_assert(r_node && r_node->type == SH_NODE_TEX_IMAGE);
/* First try treating texture path as relative. */
std::string tex_path{tex_map.mtl_dir_path + tex_map.image_path};
if (load_texture_image_at_path(bmain, tex_map, r_node, tex_path)) {
return true;
}
/* Then try using it directly as absolute path. */
std::string raw_path{tex_map.image_path};
if (load_texture_image_at_path(bmain, tex_map, r_node, raw_path)) {
return true;
}
/* Try removing quotes. */
std::string no_quote_path{replace_all_occurences(tex_path, "\"", "")};
if (no_quote_path != tex_path &&
load_texture_image_at_path(bmain, tex_map, r_node, no_quote_path)) {
return true;
}
/* Try replacing underscores with spaces. */
std::string no_underscore_path{replace_all_occurences(no_quote_path, "_", " ")};
if (no_underscore_path != no_quote_path && no_underscore_path != tex_path &&
load_texture_image_at_path(bmain, tex_map, r_node, no_underscore_path)) {
return true;
}
return false;
}
/**
* Initializes a nodetree with a p-BSDF node's BSDF socket connected to shader output node's
* surface socket.
*/
ShaderNodetreeWrap::ShaderNodetreeWrap(Main *bmain, const MTLMaterial &mtl_mat, Material *mat)
: mtl_mat_(mtl_mat)
{
nodetree_.reset(ntreeAddTree(nullptr, "Shader Nodetree", ntreeType_Shader->idname));
bsdf_ = add_node_to_tree(SH_NODE_BSDF_PRINCIPLED);
shader_output_ = add_node_to_tree(SH_NODE_OUTPUT_MATERIAL);
set_bsdf_socket_values();
add_image_textures(bmain, mat);
link_sockets(bsdf_, "BSDF", shader_output_, "Surface", 4);
nodeSetActive(nodetree_.get(), shader_output_);
}
/**
* Assert if caller hasn't acquired nodetree.
*/
ShaderNodetreeWrap::~ShaderNodetreeWrap()
{
if (nodetree_) {
/* nodetree's ownership must be acquired by the caller. */
nodetree_.reset();
BLI_assert(0);
}
}
/**
* Release nodetree for materials to own it. nodetree has its unique deleter
* if destructor is not reached for some reason.
*/
bNodeTree *ShaderNodetreeWrap::get_nodetree()
{
/* If this function has been reached, we know that nodes and the nodetree
* can be added to the scene safely. */
return nodetree_.release();
}
/**
* Add a new static node to the tree.
* No two nodes are linked here.
*/
bNode *ShaderNodetreeWrap::add_node_to_tree(const int node_type)
{
return nodeAddStaticNode(nullptr, nodetree_.get(), node_type);
}
/**
* Return x-y coordinates for a node where y is determined by other nodes present in
* the same vertical column.
*/
std::pair<float, float> ShaderNodetreeWrap::set_node_locations(const int pos_x)
{
int pos_y = 0;
bool found = false;
while (true) {
for (Span<int> location : node_locations) {
if (location[0] == pos_x && location[1] == pos_y) {
pos_y += 1;
found = true;
}
else {
found = false;
}
}
if (!found) {
node_locations.append({pos_x, pos_y});
return {pos_x * node_size_, pos_y * node_size_ * 2.0 / 3.0};
}
}
}
/**
* Link two nodes by the sockets of given IDs.
* Also releases the ownership of the "from" node for nodetree to free it.
* \param from_node_pos_x 0 to 4 value as per nodetree arrangement.
*/
void ShaderNodetreeWrap::link_sockets(bNode *from_node,
StringRef from_node_id,
bNode *to_node,
StringRef to_node_id,
const int from_node_pos_x)
{
std::tie(from_node->locx, from_node->locy) = set_node_locations(from_node_pos_x);
std::tie(to_node->locx, to_node->locy) = set_node_locations(from_node_pos_x + 1);
bNodeSocket *from_sock{nodeFindSocket(from_node, SOCK_OUT, from_node_id.data())};
bNodeSocket *to_sock{nodeFindSocket(to_node, SOCK_IN, to_node_id.data())};
BLI_assert(from_sock && to_sock);
nodeAddLink(nodetree_.get(), from_node, from_sock, to_node, to_sock);
}
/**
* Set values of sockets in p-BSDF node of the nodetree.
*/
void ShaderNodetreeWrap::set_bsdf_socket_values()
{
const int illum = mtl_mat_.illum;
bool do_highlight = false;
bool do_tranparency = false;
bool do_reflection = false;
bool do_glass = false;
/* See https://wikipedia.org/wiki/Wavefront_.obj_file for possible values of illum. */
switch (illum) {
case 1: {
/* Base color on, ambient on. */
break;
}
case 2: {
/* Highlight on. */
do_highlight = true;
break;
}
case 3: {
/* Reflection on and Ray trace on. */
do_reflection = true;
break;
}
case 4: {
/* Transparency: Glass on, Reflection: Ray trace on. */
do_glass = true;
do_reflection = true;
do_tranparency = true;
break;
}
case 5: {
/* Reflection: Fresnel on and Ray trace on. */
do_reflection = true;
break;
}
case 6: {
/* Transparency: Refraction on, Reflection: Fresnel off and Ray trace on. */
do_reflection = true;
do_tranparency = true;
break;
}
case 7: {
/* Transparency: Refraction on, Reflection: Fresnel on and Ray trace on. */
do_reflection = true;
do_tranparency = true;
break;
}
case 8: {
/* Reflection on and Ray trace off. */
do_reflection = true;
break;
}
case 9: {
/* Transparency: Glass on, Reflection: Ray trace off. */
do_glass = true;
do_reflection = false;
do_tranparency = true;
break;
}
default: {
std::cerr << "Warning! illum value = " << illum
<< "is not supported by the Principled-BSDF shader." << std::endl;
break;
}
}
/* Approximations for trying to map obj/mtl material model into
* Principled BSDF: */
/* Specular: average of Ks components. */
float specular = (mtl_mat_.Ks[0] + mtl_mat_.Ks[1] + mtl_mat_.Ks[2]) / 3;
/* Roughness: map 0..1000 range to 1..0 and apply non-linearity. */
float clamped_ns = std::max(0.0f, std::min(1000.0f, mtl_mat_.Ns));
float roughness = 1.0f - sqrt(clamped_ns / 1000.0f);
/* Metallic: average of Ka components. */
float metallic = (mtl_mat_.Ka[0] + mtl_mat_.Ka[1] + mtl_mat_.Ka[2]) / 3;
float ior = mtl_mat_.Ni;
float alpha = mtl_mat_.d;
if (specular < 0.0f) {
specular = static_cast<float>(do_highlight);
}
if (mtl_mat_.Ns < 0.0f) {
roughness = static_cast<float>(!do_highlight);
}
if (metallic < 0.0f) {
if (do_reflection) {
metallic = 1.0f;
}
}
else {
metallic = 0.0f;
}
if (ior < 0) {
if (do_tranparency) {
ior = 1.0f;
}
if (do_glass) {
ior = 1.5f;
}
}
if (alpha < 0) {
if (do_tranparency) {
alpha = 1.0f;
}
}
float3 base_color = {std::max(0.0f, mtl_mat_.Kd[0]),
std::max(0.0f, mtl_mat_.Kd[1]),
std::max(0.0f, mtl_mat_.Kd[2])};
float3 emission_color = {std::max(0.0f, mtl_mat_.Ke[0]),
std::max(0.0f, mtl_mat_.Ke[1]),
std::max(0.0f, mtl_mat_.Ke[2])};
set_property_of_socket(SOCK_RGBA, "Base Color", {base_color, 3}, bsdf_);
set_property_of_socket(SOCK_RGBA, "Emission", {emission_color, 3}, bsdf_);
if (mtl_mat_.texture_maps.contains_as(eMTLSyntaxElement::map_Ke)) {
set_property_of_socket(SOCK_FLOAT, "Emission Strength", {1.0f}, bsdf_);
}
set_property_of_socket(SOCK_FLOAT, "Specular", {specular}, bsdf_);
set_property_of_socket(SOCK_FLOAT, "Roughness", {roughness}, bsdf_);
set_property_of_socket(SOCK_FLOAT, "Metallic", {metallic}, bsdf_);
set_property_of_socket(SOCK_FLOAT, "IOR", {ior}, bsdf_);
set_property_of_socket(SOCK_FLOAT, "Alpha", {alpha}, bsdf_);
}
/**
* Create image texture, vector and normal mapping nodes from MTL materials and link the
* nodes to p-BSDF node.
*/
void ShaderNodetreeWrap::add_image_textures(Main *bmain, Material *mat)
{
for (const Map<const eMTLSyntaxElement, tex_map_XX>::Item texture_map :
mtl_mat_.texture_maps.items()) {
if (texture_map.value.image_path.empty()) {
/* No Image texture node of this map type can be added to this material. */
continue;
}
bNode *image_texture = add_node_to_tree(SH_NODE_TEX_IMAGE);
if (!load_texture_image(bmain, texture_map.value, image_texture)) {
/* Image could not be added, so don't add or link further nodes. */
continue;
}
/* Add normal map node if needed. */
bNode *normal_map = nullptr;
if (texture_map.key == eMTLSyntaxElement::map_Bump) {
normal_map = add_node_to_tree(SH_NODE_NORMAL_MAP);
const float bump = std::max(0.0f, mtl_mat_.map_Bump_strength);
set_property_of_socket(SOCK_FLOAT, "Strength", {bump}, normal_map);
}
/* Add UV mapping & coordinate nodes only if needed. */
if (texture_map.value.translation != float3(0, 0, 0) ||
texture_map.value.scale != float3(1, 1, 1)) {
bNode *mapping = add_node_to_tree(SH_NODE_MAPPING);
bNode *texture_coordinate = add_node_to_tree(SH_NODE_TEX_COORD);
set_property_of_socket(SOCK_VECTOR, "Location", {texture_map.value.translation, 3}, mapping);
set_property_of_socket(SOCK_VECTOR, "Scale", {texture_map.value.scale, 3}, mapping);
link_sockets(texture_coordinate, "UV", mapping, "Vector", 0);
link_sockets(mapping, "Vector", image_texture, "Vector", 1);
}
if (normal_map) {
link_sockets(image_texture, "Color", normal_map, "Color", 2);
link_sockets(normal_map, "Normal", bsdf_, "Normal", 3);
}
else if (texture_map.key == eMTLSyntaxElement::map_d) {
link_sockets(image_texture, "Alpha", bsdf_, texture_map.value.dest_socket_id, 2);
mat->blend_method = MA_BM_BLEND;
}
else {
link_sockets(image_texture, "Color", bsdf_, texture_map.value.dest_socket_id, 2);
}
}
}
} // namespace blender::io::obj

View File

@ -0,0 +1,90 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include <array>
#include "BLI_map.hh"
#include "BLI_math_vec_types.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "DNA_node_types.h"
#include "MEM_guardedalloc.h"
#include "obj_export_mtl.hh"
namespace blender::io::obj {
struct UniqueNodetreeDeleter {
void operator()(bNodeTree *node)
{
MEM_freeN(node);
}
};
using unique_nodetree_ptr = std::unique_ptr<bNodeTree, UniqueNodetreeDeleter>;
class ShaderNodetreeWrap {
private:
/* Node arrangement:
* Texture Coordinates -> Mapping -> Image Texture -> (optional) Normal Map -> p-BSDF -> Material
* Output. */
unique_nodetree_ptr nodetree_;
bNode *bsdf_;
bNode *shader_output_;
const MTLMaterial &mtl_mat_;
/* List of all locations occupied by nodes. */
Vector<std::array<int, 2>> node_locations;
const float node_size_{300.f};
public:
ShaderNodetreeWrap(Main *bmain, const MTLMaterial &mtl_mat, Material *mat);
~ShaderNodetreeWrap();
bNodeTree *get_nodetree();
private:
bNode *add_node_to_tree(const int node_type);
std::pair<float, float> set_node_locations(const int pos_x);
void link_sockets(bNode *from_node,
StringRef from_node_id,
bNode *to_node,
StringRef to_node_id,
const int from_node_pos_x);
void set_bsdf_socket_values();
void add_image_textures(Main *bmain, Material *mat);
};
constexpr eMTLSyntaxElement mtl_line_key_str_to_enum(const std::string_view key_str)
{
if (key_str == "map_Kd") {
return eMTLSyntaxElement::map_Kd;
}
if (key_str == "map_Ks") {
return eMTLSyntaxElement::map_Ks;
}
if (key_str == "map_Ns") {
return eMTLSyntaxElement::map_Ns;
}
if (key_str == "map_d") {
return eMTLSyntaxElement::map_d;
}
if (key_str == "refl" || key_str == "map_refl") {
return eMTLSyntaxElement::map_refl;
}
if (key_str == "map_Ke") {
return eMTLSyntaxElement::map_Ke;
}
if (key_str == "map_Bump" || key_str == "bump") {
return eMTLSyntaxElement::map_Bump;
}
return eMTLSyntaxElement::string;
}
} // namespace blender::io::obj

View File

@ -0,0 +1,99 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include "BKE_object.h"
#include "BLI_math_vector.h"
#include "DNA_curve_types.h"
#include "importer_mesh_utils.hh"
#include "obj_import_nurbs.hh"
#include "obj_import_objects.hh"
namespace blender::io::obj {
Object *CurveFromGeometry::create_curve(Main *bmain, const OBJImportParams &import_params)
{
std::string ob_name{curve_geometry_.geometry_name_};
if (ob_name.empty() && !curve_geometry_.nurbs_element_.group_.empty()) {
ob_name = curve_geometry_.nurbs_element_.group_;
}
if (ob_name.empty()) {
ob_name = "Untitled";
}
BLI_assert(!curve_geometry_.nurbs_element_.curv_indices.is_empty());
Curve *curve = BKE_curve_add(bmain, ob_name.c_str(), OB_CURVES_LEGACY);
Object *obj = BKE_object_add_only_object(bmain, OB_CURVES_LEGACY, ob_name.c_str());
curve->flag = CU_3D;
curve->resolu = curve->resolv = 12;
/* Only one NURBS spline will be created in the curve object. */
curve->actnu = 0;
Nurb *nurb = static_cast<Nurb *>(MEM_callocN(sizeof(Nurb), "OBJ import NURBS curve"));
BLI_addtail(BKE_curve_nurbs_get(curve), nurb);
create_nurbs(curve);
obj->data = curve;
transform_object(obj, import_params);
return obj;
}
/**
* Create a NURBS spline for the Curve converted from Geometry.
*/
void CurveFromGeometry::create_nurbs(Curve *curve)
{
const NurbsElement &nurbs_geometry = curve_geometry_.nurbs_element_;
Nurb *nurb = static_cast<Nurb *>(curve->nurb.first);
nurb->type = CU_NURBS;
nurb->flag = CU_3D;
nurb->next = nurb->prev = nullptr;
/* BKE_nurb_points_add later on will update pntsu. If this were set to total curv points,
* we get double the total points in viewport. */
nurb->pntsu = 0;
/* Total points = pntsu * pntsv. */
nurb->pntsv = 1;
nurb->orderu = nurb->orderv = (nurbs_geometry.degree + 1 > SHRT_MAX) ? 4 :
nurbs_geometry.degree + 1;
nurb->resolu = nurb->resolv = curve->resolu;
const int64_t tot_vert{nurbs_geometry.curv_indices.size()};
BKE_nurb_points_add(nurb, tot_vert);
for (int i = 0; i < tot_vert; i++) {
BPoint &bpoint = nurb->bp[i];
copy_v3_v3(bpoint.vec, global_vertices_.vertices[nurbs_geometry.curv_indices[i]]);
bpoint.vec[3] = 1.0f;
bpoint.weight = 1.0f;
}
BKE_nurb_knot_calc_u(nurb);
bool do_endpoints = false;
int deg1 = nurbs_geometry.degree + 1;
if (nurbs_geometry.parm.size() >= deg1 * 2) {
do_endpoints = true;
for (int i = 0; i < deg1; ++i) {
if (abs(nurbs_geometry.parm[i]) > 0.0001f) {
do_endpoints = false;
break;
}
if (abs(nurbs_geometry.parm[nurbs_geometry.parm.size() - 1 - i] - 1.0f) > 0.0001f) {
do_endpoints = false;
break;
}
}
}
if (do_endpoints) {
nurb->flagu = CU_NURB_ENDPOINT;
}
}
} // namespace blender::io::obj

View File

@ -0,0 +1,38 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "BKE_curve.h"
#include "BLI_utility_mixins.hh"
#include "DNA_curve_types.h"
#include "obj_import_objects.hh"
namespace blender::io::obj {
/**
* Make a Blender NURBS Curve block from a Geometry of GEOM_CURVE type.
*/
class CurveFromGeometry : NonMovable, NonCopyable {
private:
const Geometry &curve_geometry_;
const GlobalVertices &global_vertices_;
public:
CurveFromGeometry(const Geometry &geometry, const GlobalVertices &global_vertices)
: curve_geometry_(geometry), global_vertices_(global_vertices)
{
}
Object *create_curve(Main *bmain, const OBJImportParams &import_params);
private:
void create_nurbs(Curve *curve);
};
} // namespace blender::io::obj

View File

@ -0,0 +1,111 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "BKE_lib_id.h"
#include "BLI_math_vec_types.hh"
#include "BLI_vector.hh"
#include "BLI_vector_set.hh"
#include "DNA_meshdata_types.h"
#include "DNA_object_types.h"
namespace blender::io::obj {
/**
* List of all vertex and UV vertex coordinates in an OBJ file accessible to any
* Geometry instance at any time.
*/
struct GlobalVertices {
Vector<float3> vertices;
Vector<float2> uv_vertices;
Vector<float3> vertex_normals;
};
/**
* Keeps track of the vertices that belong to other Geometries.
* Needed only for MLoop.v and MEdge.v1 which needs vertex indices ranging from (0 to total
* vertices in the mesh) as opposed to the other OBJ indices ranging from (0 to total vertices
* in the global list).
*/
struct VertexIndexOffset {
private:
int offset_ = 0;
public:
void set_index_offset(const int64_t total_vertices)
{
offset_ = total_vertices;
}
int64_t get_index_offset() const
{
return offset_;
}
};
/**
* A face's corner in an OBJ file. In Blender, it translates to a mloop vertex.
*/
struct PolyCorner {
/* These indices range from zero to total vertices in the OBJ file. */
int vert_index;
/* -1 is to indicate absence of UV vertices. Only < 0 condition should be checked since
* it can be less than -1 too. */
int uv_vert_index = -1;
int vertex_normal_index = -1;
};
struct PolyElem {
std::string vertex_group;
std::string material_name;
bool shaded_smooth = false;
Vector<PolyCorner> face_corners;
};
/**
* Contains data for one single NURBS curve in the OBJ file.
*/
struct NurbsElement {
/**
* For curves, groups may be used to specify multiple splines in the same curve object.
* It may also serve as the name of the curve if not specified explicitly.
*/
std::string group_;
int degree = 0;
/**
* Indices into the global list of vertex coordinates. Must be non-negative.
*/
Vector<int> curv_indices;
/* Values in the parm u/v line in a curve definition. */
Vector<float> parm;
};
enum eGeometryType {
GEOM_MESH = OB_MESH,
GEOM_CURVE = OB_CURVES_LEGACY,
};
struct Geometry {
eGeometryType geom_type_ = GEOM_MESH;
std::string geometry_name_;
VectorSet<std::string> material_names_;
/**
* Indices in the vector range from zero to total vertices in a geometry.
* Values range from zero to total coordinates in the global list.
*/
Vector<int> vertex_indices_;
/** Edges written in the file in addition to (or even without polygon) elements. */
Vector<MEdge> edges_;
Vector<PolyElem> face_elements_;
bool has_vertex_normals_ = false;
bool use_vertex_groups_ = false;
NurbsElement nurbs_element_;
int total_loops_ = 0;
};
} // namespace blender::io::obj

View File

@ -0,0 +1,111 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include <string>
#include "BLI_map.hh"
#include "BLI_math_vec_types.hh"
#include "BLI_set.hh"
#include "BLI_string_ref.hh"
#include "BKE_layer.h"
#include "BKE_scene.h"
#include "DEG_depsgraph_build.h"
#include "DNA_collection_types.h"
#include "obj_import_file_reader.hh"
#include "obj_import_mesh.hh"
#include "obj_import_nurbs.hh"
#include "obj_import_objects.hh"
#include "obj_importer.hh"
namespace blender::io::obj {
/**
* Make Blender Mesh, Curve etc from Geometry and add them to the import collection.
*/
static void geometry_to_blender_objects(
Main *bmain,
Scene *scene,
ViewLayer *view_layer,
const OBJImportParams &import_params,
Vector<std::unique_ptr<Geometry>> &all_geometries,
const GlobalVertices &global_vertices,
const Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
Map<std::string, Material *> &created_materials)
{
BKE_view_layer_base_deselect_all(view_layer);
LayerCollection *lc = BKE_layer_collection_get_active(view_layer);
for (const std::unique_ptr<Geometry> &geometry : all_geometries) {
Object *obj = nullptr;
if (geometry->geom_type_ == GEOM_MESH) {
MeshFromGeometry mesh_ob_from_geometry{*geometry, global_vertices};
obj = mesh_ob_from_geometry.create_mesh(bmain, materials, created_materials, import_params);
}
else if (geometry->geom_type_ == GEOM_CURVE) {
CurveFromGeometry curve_ob_from_geometry(*geometry, global_vertices);
obj = curve_ob_from_geometry.create_curve(bmain, import_params);
}
if (obj != nullptr) {
BKE_collection_object_add(bmain, lc->collection, obj);
Base *base = BKE_view_layer_base_find(view_layer, obj);
/* TODO: is setting active needed? */
BKE_view_layer_base_select_and_set_active(view_layer, base);
DEG_id_tag_update(&lc->collection->id, ID_RECALC_COPY_ON_WRITE);
DEG_id_tag_update_ex(bmain,
&obj->id,
ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY | ID_RECALC_ANIMATION |
ID_RECALC_BASE_FLAGS);
}
}
DEG_id_tag_update(&scene->id, ID_RECALC_BASE_FLAGS);
DEG_relations_tag_update(bmain);
}
void importer_main(bContext *C, const OBJImportParams &import_params)
{
Main *bmain = CTX_data_main(C);
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
importer_main(bmain, scene, view_layer, import_params);
static_cast<void>(CTX_data_ensure_evaluated_depsgraph(C));
}
void importer_main(Main *bmain,
Scene *scene,
ViewLayer *view_layer,
const OBJImportParams &import_params)
{
/* List of Geometry instances to be parsed from OBJ file. */
Vector<std::unique_ptr<Geometry>> all_geometries;
/* Container for vertex and UV vertex coordinates. */
GlobalVertices global_vertices;
/* List of MTLMaterial instances to be parsed from MTL file. */
Map<std::string, std::unique_ptr<MTLMaterial>> materials;
Map<std::string, Material *> created_materials;
OBJParser obj_parser{import_params};
obj_parser.parse(all_geometries, global_vertices);
for (StringRef mtl_library : obj_parser.mtl_libraries()) {
MTLParser mtl_parser{mtl_library, import_params.filepath};
mtl_parser.parse_and_store(materials);
}
geometry_to_blender_objects(bmain,
scene,
view_layer,
import_params,
all_geometries,
global_vertices,
materials,
created_materials);
}
} // namespace blender::io::obj

View File

@ -0,0 +1,22 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "IO_wavefront_obj.h"
namespace blender::io::obj {
/* Main import function used from within Blender. */
void importer_main(bContext *C, const OBJImportParams &import_params);
/* Used from tests, where full bContext does not exist. */
void importer_main(Main *bmain,
Scene *scene,
ViewLayer *view_layer,
const OBJImportParams &import_params);
} // namespace blender::io::obj

View File

@ -0,0 +1,209 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include <fstream>
#include <iostream>
#include <sstream>
#include "BLI_math_vec_types.hh"
#include "BLI_span.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "parser_string_utils.hh"
/* Note: these OBJ parser helper functions are planned to get fairly large
* changes "soon", so don't read too much into current implementation... */
namespace blender::io::obj {
using std::string;
/**
* Store multiple lines separated by an escaped newline character: `\\n`.
* Use this before doing any parse operations on the read string.
*/
void read_next_line(std::fstream &file, string &r_line)
{
std::string new_line;
while (file.good() && !r_line.empty() && r_line.back() == '\\') {
new_line.clear();
const bool ok = static_cast<bool>(std::getline(file, new_line));
/* Remove the last backslash character. */
r_line.pop_back();
r_line.append(new_line);
if (!ok || new_line.empty()) {
return;
}
}
}
/**
* Split a line string into the first word (key) and the rest of the line.
* Also remove leading & trailing spaces as well as `\r` carriage return
* character if present.
*/
void split_line_key_rest(const StringRef line, StringRef &r_line_key, StringRef &r_rest_line)
{
if (line.is_empty()) {
return;
}
const int64_t pos_split{line.find_first_of(' ')};
if (pos_split == StringRef::not_found) {
/* Use the first character if no space is found in the line. It's usually a comment like:
* #This is a comment. */
r_line_key = line.substr(0, 1);
}
else {
r_line_key = line.substr(0, pos_split);
}
/* Eat the delimiter also using "+ 1". */
r_rest_line = line.drop_prefix(r_line_key.size() + 1);
if (r_rest_line.is_empty()) {
return;
}
/* Remove any leading spaces, trailing spaces & \r character, if any. */
const int64_t leading_space{r_rest_line.find_first_not_of(' ')};
if (leading_space != StringRef::not_found) {
r_rest_line = r_rest_line.drop_prefix(leading_space);
}
/* Another way is to do a test run before the actual parsing to find the newline
* character and use it in the getline. */
const int64_t carriage_return{r_rest_line.find_first_of('\r')};
if (carriage_return != StringRef::not_found) {
r_rest_line = r_rest_line.substr(0, carriage_return + 1);
}
const int64_t trailing_space{r_rest_line.find_last_not_of(' ')};
if (trailing_space != StringRef::not_found) {
/* The position is of a character that is not ' ', so count of characters is position + 1. */
r_rest_line = r_rest_line.substr(0, trailing_space + 1);
}
}
/**
* Split the given string by the delimiter and fill the given vector.
* If an intermediate string is empty, or space or null character, it is not appended to the
* vector.
*/
void split_by_char(StringRef in_string, const char delimiter, Vector<StringRef> &r_out_list)
{
r_out_list.clear();
while (!in_string.is_empty()) {
const int64_t pos_delim{in_string.find_first_of(delimiter)};
const int64_t word_len = pos_delim == StringRef::not_found ? in_string.size() : pos_delim;
StringRef word{in_string.data(), word_len};
if (!word.is_empty() && !(word == " " && !(word[0] == '\0'))) {
r_out_list.append(word);
}
if (pos_delim == StringRef::not_found) {
return;
}
/* Skip the word already stored. */
in_string = in_string.drop_prefix(word_len);
/* Skip all delimiters. */
const int64_t pos_non_delim = in_string.find_first_not_of(delimiter);
if (pos_non_delim == StringRef::not_found) {
return;
}
in_string = in_string.drop_prefix(std::min(pos_non_delim, in_string.size()));
}
}
/**
* Convert the given string to float and assign it to the destination value.
*
* If the string cannot be converted to a float, the fallback value is used.
*/
void copy_string_to_float(StringRef src, const float fallback_value, float &r_dst)
{
try {
r_dst = std::stof(string(src));
}
catch (const std::invalid_argument &inv_arg) {
std::cerr << "Bad conversion to float:'" << inv_arg.what() << "':'" << src << "'" << std::endl;
r_dst = fallback_value;
}
catch (const std::out_of_range &out_of_range) {
std::cerr << "Out of range for float:'" << out_of_range.what() << ":'" << src << "'"
<< std::endl;
r_dst = fallback_value;
}
}
/**
* Convert all members of the Span of strings to floats and assign them to the float
* array members. Usually used for values like coordinates.
*
* If a string cannot be converted to a float, the fallback value is used.
*/
void copy_string_to_float(Span<StringRef> src,
const float fallback_value,
MutableSpan<float> r_dst)
{
for (int i = 0; i < r_dst.size(); ++i) {
if (i < src.size()) {
copy_string_to_float(src[i], fallback_value, r_dst[i]);
}
else {
r_dst[i] = fallback_value;
}
}
}
/**
* Convert the given string to int and assign it to the destination value.
*
* If the string cannot be converted to an integer, the fallback value is used.
*/
void copy_string_to_int(StringRef src, const int fallback_value, int &r_dst)
{
try {
r_dst = std::stoi(string(src));
}
catch (const std::invalid_argument &inv_arg) {
std::cerr << "Bad conversion to int:'" << inv_arg.what() << "':'" << src << "'" << std::endl;
r_dst = fallback_value;
}
catch (const std::out_of_range &out_of_range) {
std::cerr << "Out of range for int:'" << out_of_range.what() << ":'" << src << "'"
<< std::endl;
r_dst = fallback_value;
}
}
/**
* Convert the given strings to ints and fill the destination int buffer.
*
* If a string cannot be converted to an integer, the fallback value is used.
*/
void copy_string_to_int(Span<StringRef> src, const int fallback_value, MutableSpan<int> r_dst)
{
for (int i = 0; i < r_dst.size(); ++i) {
if (i < src.size()) {
copy_string_to_int(src[i], fallback_value, r_dst[i]);
}
else {
r_dst[i] = fallback_value;
}
}
}
std::string replace_all_occurences(StringRef original, StringRef to_remove, StringRef to_add)
{
std::string clean{original};
while (true) {
const std::string::size_type pos = clean.find(to_remove);
if (pos == std::string::npos) {
break;
}
clean.replace(pos, to_add.size(), to_add);
}
return clean;
}
} // namespace blender::io::obj

View File

@ -0,0 +1,19 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
namespace blender::io::obj {
/* Note: these OBJ parser helper functions are planned to get fairly large
* changes "soon", so don't read too much into current implementation... */
void read_next_line(std::fstream &file, std::string &r_line);
void split_line_key_rest(StringRef line, StringRef &r_line_key, StringRef &r_rest_line);
void split_by_char(StringRef in_string, const char delimiter, Vector<StringRef> &r_out_list);
void copy_string_to_float(StringRef src, const float fallback_value, float &r_dst);
void copy_string_to_float(Span<StringRef> src,
const float fallback_value,
MutableSpan<float> r_dst);
void copy_string_to_int(StringRef src, const int fallback_value, int &r_dst);
void copy_string_to_int(Span<StringRef> src, const int fallback_value, MutableSpan<int> r_dst);
std::string replace_all_occurences(StringRef original, StringRef to_remove, StringRef to_add);
} // namespace blender::io::obj

View File

@ -0,0 +1,491 @@
/* SPDX-License-Identifier: Apache-2.0 */
#include <gtest/gtest.h>
#include "testing/testing.h"
#include "tests/blendfile_loading_base_test.h"
#include "BKE_curve.h"
#include "BKE_customdata.h"
#include "BKE_main.h"
#include "BKE_object.h"
#include "BKE_scene.h"
#include "BLI_listbase.h"
#include "BLI_math_base.h"
#include "BLI_math_vec_types.hh"
#include "BLO_readfile.h"
#include "DEG_depsgraph.h"
#include "DEG_depsgraph_query.h"
#include "DNA_curve_types.h"
#include "DNA_mesh_types.h"
#include "DNA_meshdata_types.h"
#include "DNA_scene_types.h"
#include "MEM_guardedalloc.h"
#include "obj_importer.hh"
namespace blender::io::obj {
struct Expectation {
std::string name;
short type; /* OB_MESH, ... */
int totvert, mesh_totedge_or_curve_endp, mesh_totpoly_or_curve_order,
mesh_totloop_or_curve_cyclic;
float3 vert_first, vert_last;
float3 normal_first;
float2 uv_first;
};
class obj_importer_test : public BlendfileLoadingBaseTest {
public:
void import_and_check(const char *path,
const Expectation *expect,
size_t expect_count,
int expect_mat_count)
{
if (!blendfile_load("io_tests/blend_geometry/all_quads.blend")) {
ADD_FAILURE();
return;
}
OBJImportParams params;
params.clamp_size = 0;
params.forward_axis = OBJ_AXIS_NEGATIVE_Z_FORWARD;
params.up_axis = OBJ_AXIS_Y_UP;
std::string obj_path = blender::tests::flags_test_asset_dir() + "/io_tests/obj/" + path;
strncpy(params.filepath, obj_path.c_str(), FILE_MAX - 1);
importer_main(bfile->main, bfile->curscene, bfile->cur_view_layer, params);
depsgraph_create(DAG_EVAL_VIEWPORT);
const int deg_objects_visibility_flags = DEG_ITER_OBJECT_FLAG_LINKED_DIRECTLY |
DEG_ITER_OBJECT_FLAG_LINKED_VIA_SET |
DEG_ITER_OBJECT_FLAG_VISIBLE |
DEG_ITER_OBJECT_FLAG_DUPLI;
size_t object_index = 0;
DEG_OBJECT_ITER_BEGIN (depsgraph, object, deg_objects_visibility_flags) {
if (object_index >= expect_count) {
ADD_FAILURE();
break;
}
const Expectation &exp = expect[object_index];
ASSERT_STREQ(object->id.name, exp.name.c_str());
EXPECT_EQ(object->type, exp.type);
EXPECT_V3_NEAR(object->loc, float3(0, 0, 0), 0.0001f);
if (strcmp(object->id.name, "OBCube") != 0) {
EXPECT_V3_NEAR(object->rot, float3(M_PI_2, 0, 0), 0.0001f);
}
EXPECT_V3_NEAR(object->scale, float3(1, 1, 1), 0.0001f);
if (object->type == OB_MESH || object->type == OB_SURF) {
Mesh *mesh = BKE_object_get_evaluated_mesh(object);
EXPECT_EQ(mesh->totvert, exp.totvert);
EXPECT_EQ(mesh->totedge, exp.mesh_totedge_or_curve_endp);
EXPECT_EQ(mesh->totpoly, exp.mesh_totpoly_or_curve_order);
EXPECT_EQ(mesh->totloop, exp.mesh_totloop_or_curve_cyclic);
EXPECT_V3_NEAR(mesh->mvert[0].co, exp.vert_first, 0.0001f);
EXPECT_V3_NEAR(mesh->mvert[mesh->totvert - 1].co, exp.vert_last, 0.0001f);
const float3 *lnors = (const float3 *)(CustomData_get_layer(&mesh->ldata, CD_NORMAL));
float3 normal_first = lnors != nullptr ? lnors[0] : float3(0, 0, 0);
EXPECT_V3_NEAR(normal_first, exp.normal_first, 0.0001f);
const MLoopUV *mloopuv = static_cast<const MLoopUV *>(
CustomData_get_layer(&mesh->ldata, CD_MLOOPUV));
float2 uv_first = mloopuv ? float2(mloopuv->uv) : float2(0, 0);
EXPECT_V2_NEAR(uv_first, exp.uv_first, 0.0001f);
}
if (object->type == OB_CURVES_LEGACY) {
Curve *curve = static_cast<Curve *>(DEG_get_evaluated_object(depsgraph, object)->data);
int numVerts;
float(*vertexCos)[3] = BKE_curve_nurbs_vert_coords_alloc(&curve->nurb, &numVerts);
EXPECT_EQ(numVerts, exp.totvert);
EXPECT_V3_NEAR(vertexCos[0], exp.vert_first, 0.0001f);
EXPECT_V3_NEAR(vertexCos[numVerts - 1], exp.vert_last, 0.0001f);
MEM_freeN(vertexCos);
const Nurb *nurb = static_cast<const Nurb *>(BLI_findlink(&curve->nurb, 0));
int endpoint = (nurb->flagu & CU_NURB_ENDPOINT) ? 1 : 0;
EXPECT_EQ(nurb->orderu, exp.mesh_totpoly_or_curve_order);
EXPECT_EQ(endpoint, exp.mesh_totedge_or_curve_endp);
// Cyclic flag is not set by the importer yet
// int cyclic = (nurb->flagu & CU_NURB_CYCLIC) ? 1 : 0;
// EXPECT_EQ(cyclic, exp.mesh_totloop_or_curve_cyclic);
}
++object_index;
}
DEG_OBJECT_ITER_END;
EXPECT_EQ(object_index, expect_count);
/* Count number of materials. */
int mat_count = 0;
LISTBASE_FOREACH (ID *, id, &bfile->main->materials) {
++mat_count;
}
EXPECT_EQ(mat_count, expect_mat_count);
}
};
TEST_F(obj_importer_test, import_cube)
{
Expectation expect[] = {
{"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
{"OBNew object",
OB_MESH,
8,
12,
6,
24,
float3(-1, -1, 1),
float3(1, -1, -1),
float3(-0.57735f, 0.57735f, -0.57735f)},
};
import_and_check("cube.obj", expect, std::size(expect), 1);
}
TEST_F(obj_importer_test, import_suzanne_all_data)
{
Expectation expect[] = {
{"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
{"OBMonkey",
OB_MESH,
505,
1005,
500,
1968,
float3(-0.4375f, 0.164062f, 0.765625f),
float3(0.4375f, 0.164062f, 0.765625f),
float3(-0.6040f, -0.5102f, 0.6122f),
float2(0.692094f, 0.40191f)},
};
import_and_check("suzanne_all_data.obj", expect, std::size(expect), 0);
}
TEST_F(obj_importer_test, import_nurbs)
{
Expectation expect[] = {
{"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
{"OBNew object",
OB_CURVES_LEGACY,
12,
0,
4,
1,
float3(0.260472f, -1.477212f, -0.866025f),
float3(-1.5f, 2.598076f, 0)},
};
import_and_check("nurbs.obj", expect, std::size(expect), 0);
}
TEST_F(obj_importer_test, import_nurbs_curves)
{
Expectation expect[] = {
{"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
{"OBNew object", OB_CURVES_LEGACY, 4, 0, 4, 0, float3(2, -2, 0), float3(-2, -2, 0)},
{"OBNurbsCurveDiffWeights",
OB_CURVES_LEGACY,
4,
0,
4,
0,
float3(6, -2, 0),
float3(2, -2, 0)},
{"OBNurbsCurveCyclic", OB_CURVES_LEGACY, 7, 0, 4, 1, float3(-2, -2, 0), float3(-6, 2, 0)},
{"OBNurbsCurveEndpoint",
OB_CURVES_LEGACY,
4,
1,
4,
0,
float3(-6, -2, 0),
float3(-10, -2, 0)},
{"OBCurveDeg3", OB_CURVES_LEGACY, 4, 0, 3, 0, float3(10, -2, 0), float3(6, -2, 0)},
};
import_and_check("nurbs_curves.obj", expect, std::size(expect), 0);
}
TEST_F(obj_importer_test, import_nurbs_cyclic)
{
Expectation expect[] = {
{"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
{"OBNew object",
OB_CURVES_LEGACY,
31,
0,
4,
1,
float3(2.591002f, 0, -0.794829f),
float3(3.280729f, 0, 3.043217f)},
};
import_and_check("nurbs_cyclic.obj", expect, std::size(expect), 0);
}
TEST_F(obj_importer_test, import_nurbs_manual)
{
Expectation expect[] = {
{"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
{"OBCurve_Uniform_Parm", OB_CURVES_LEGACY, 5, 0, 4, 0, float3(-2, 0, 2), float3(-2, 0, 2)},
{"OBCurve_NonUniform_Parm",
OB_CURVES_LEGACY,
5,
0,
4,
0,
float3(-2, 0, 2),
float3(-2, 0, 2)},
{"OBCurve_Endpoints", OB_CURVES_LEGACY, 5, 1, 4, 0, float3(-2, 0, 2), float3(-2, 0, 2)},
{"OBCurve_Cyclic", OB_CURVES_LEGACY, 7, 0, 4, 1, float3(-2, 0, 2), float3(2, 0, -2)},
};
import_and_check("nurbs_manual.obj", expect, std::size(expect), 0);
}
TEST_F(obj_importer_test, import_nurbs_mesh)
{
Expectation expect[] = {
{"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
{"OBTorus Knot",
OB_MESH,
108,
108,
0,
0,
float3(0.438725f, 1.070313f, 0.433013f),
float3(0.625557f, 1.040691f, 0.460328f)},
};
import_and_check("nurbs_mesh.obj", expect, std::size(expect), 0);
}
TEST_F(obj_importer_test, import_materials)
{
Expectation expect[] = {
{"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
{"OBNew object", OB_MESH, 8, 12, 6, 24, float3(-1, -1, 1), float3(1, -1, -1)},
};
import_and_check("materials.obj", expect, std::size(expect), 4);
}
TEST_F(obj_importer_test, import_faces_invalid_or_with_holes)
{
Expectation expect[] = {
{"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
{"OBFaceWithHole_BecomesTwoFacesFormingAHole",
OB_MESH,
8,
10,
2,
12,
float3(-2, 0, -2),
float3(1, 0, -1)},
{"OBFaceQuadDupSomeVerts_BecomesOneQuadUsing4Verts",
OB_MESH,
8,
4,
1,
4,
float3(3, 0, -2),
float3(6, 0, -1)},
{"OBFaceTriDupVert_Becomes1Tri", OB_MESH, 8, 3, 1, 3, float3(-2, 0, 3), float3(1, 0, 4)},
{"OBFaceAllVertsDup_BecomesOneOverlappingFaceUsingAllVerts",
OB_MESH,
8,
8,
1,
8,
float3(3, 0, 3),
float3(6, 0, 4)},
{"OBFaceAllVerts_BecomesOneOverlappingFaceUsingAllVerts",
OB_MESH,
8,
8,
1,
8,
float3(8, 0, -2),
float3(11, 0, -1)},
{"OBFaceJustTwoVerts_IsSkipped", OB_MESH, 8, 0, 0, 0, float3(8, 0, 3), float3(11, 0, 4)},
};
import_and_check("faces_invalid_or_with_holes.obj", expect, std::size(expect), 0);
}
TEST_F(obj_importer_test, import_invalid_indices)
{
Expectation expect[] = {
{"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
{"OBQuad",
OB_MESH,
4,
3,
1,
3,
float3(-2, 0, -2),
float3(2, 0, -2),
float3(0, 1, 0),
float2(0.5f, 0.25f)},
};
import_and_check("invalid_indices.obj", expect, std::size(expect), 0);
}
TEST_F(obj_importer_test, import_invalid_syntax)
{
Expectation expect[] = {
{"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
{"OBObjectWithAReallyLongNameToCheckHowImportHandlesNamesThatAreLon",
OB_MESH,
10, /* Note: right now parses some invalid obj syntax as valid vertices. */
3,
1,
3,
float3(1, 2, 3),
float3(10, 11, 12),
float3(0.4082f, -0.8165f, 0.4082f),
float2(0, 0)},
};
import_and_check("invalid_syntax.obj", expect, std::size(expect), 0);
}
TEST_F(obj_importer_test, import_all_objects)
{
Expectation expect[] = {
{"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
/* .obj file has empty EmptyText and EmptyMesh objects; these are ignored and skipped */
{"OBSurfPatch",
OB_MESH,
256,
480,
225,
900,
float3(12.5f, -2.5f, 0.694444f),
float3(13.5f, -1.5f, 0.694444f),
float3(-0.3246f, -0.3531f, 0.8775f),
float2(0, 0.066667f)},
{"OBSurfSphere",
OB_MESH,
640,
1248,
608,
2432,
float3(11, -2, -1),
float3(11, -2, 1),
float3(-0.0541f, -0.0541f, -0.9971f),
float2(0, 1)},
{"OBSmoothCube",
OB_MESH,
8,
13,
7,
26,
float3(4, 1, -1),
float3(2, 1, 1),
float3(0.5774f, 0.5773f, 0.5774f)},
{"OBMaterialCube",
OB_MESH,
8,
13,
7,
26,
float3(28, 1, -1),
float3(26, 1, 1),
float3(-1, 0, 0)},
{"OBSubSurfCube",
OB_MESH,
106,
208,
104,
416,
float3(24.444445f, 0.444444f, -0.666667f),
float3(23.790743f, 0.490725f, -0.816819f),
float3(0.1697f, 0.1697f, 0.9708f)},
{"OBParticleCube",
OB_MESH,
8,
13,
7,
26,
float3(22, 1, -1),
float3(20, 1, 1),
float3(0, 0, 1)},
{"OBShapeKeyCube",
OB_MESH,
8,
13,
7,
26,
float3(19, 1, -1),
float3(17, 1, 1),
float3(-0.4082f, -0.4082f, 0.8165f)},
{"OBUVImageCube",
OB_MESH,
8,
13,
7,
26,
float3(10, 1, -1),
float3(8, 1, 1),
float3(0, 0, 1),
float2(0.654526f, 0.579873f)},
{"OBVGroupCube",
OB_MESH,
8,
13,
7,
26,
float3(16, 1, -1),
float3(14, 1, 1),
float3(0, 0, 1)},
{"OBVColCube", OB_MESH, 8, 13, 7, 26, float3(13, 1, -1), float3(11, 1, 1), float3(0, 0, 1)},
{"OBUVCube",
OB_MESH,
8,
13,
7,
26,
float3(7, 1, -1),
float3(5, 1, 1),
float3(0, 0, 1),
float2(0.654526f, 0.579873f)},
{"OBSurface",
OB_MESH,
256,
480,
224,
896,
float3(7.292893f, -2.707107f, -1),
float3(7.525872f, -2.883338f, 1),
float3(-0.7071f, -0.7071f, 0),
float2(0, 0.142857f)},
{"OBText",
OB_MESH,
177,
345,
171,
513,
float3(1.75f, -9.458f, 0),
float3(0.587f, -9.406f, 0),
float3(0, 0, 1),
float2(0.017544f, 0)},
{"OBSurfTorus.001",
OB_MESH,
1024,
2048,
1024,
4096,
float3(5.34467f, -2.65533f, -0.176777f),
float3(5.232792f, -2.411795f, -0.220835f),
float3(-0.5042f, -0.5042f, -0.7011f),
float2(0, 1)},
{"OBNurbsCircle",
OB_MESH,
96,
96,
0,
0,
float3(3.292893f, -2.707107f, 0),
float3(3.369084f, -2.77607f, 0)},
{"OBBezierCurve", OB_MESH, 13, 12, 0, 0, float3(-1, -2, 0), float3(1, -2, 0)},
{"OBBlankCube", OB_MESH, 8, 13, 7, 26, float3(1, 1, -1), float3(-1, 1, 1), float3(0, 0, 1)},
};
import_and_check("all_objects.obj", expect, std::size(expect), 7);
}
} // namespace blender::io::obj