Add a new C++ version of an exporter for the Wavefront .obj format.

This was originally written by Ankit Meel as a GSoC 2020 project.
Howard Trickey added some tests and made some corrections/modifications.
See D13046 for more details.

This commit inserts a new menu item into the export menu called
"Wavefront OBJ (.obj) - New".
For now the old Python exporter remains in the menu, along with
the Python importer, but we plan to remove it soon (leaving the
old addon bundled with Blender but not enabled by default).
This commit is contained in:
Howard Trickey 2022-01-03 14:49:31 -05:00
parent c6069c439c
commit 4e44cfa3d9
Notes: blender-bot 2023-02-13 15:46:53 +01:00
Referenced by issue #97142, Regression: When Exporting Grease Pencil, File Dialog Shows No Subfolders
26 changed files with 3961 additions and 0 deletions

View File

@ -481,6 +481,7 @@ class TOPBAR_MT_file_export(Menu):
bl_owner_use_filter = False
def draw(self, _context):
self.layout.operator("wm.obj_export", text="Wavefront OBJ (.obj) - New")
if bpy.app.build_options.collada:
self.layout.operator("wm.collada_export",
text="Collada (Default) (.dae)")

View File

@ -26,6 +26,7 @@ set(INC
../blentranslation
../depsgraph
../draw
../editors/include
../imbuf
../makesdna
../makesrna

View File

@ -27,9 +27,11 @@
#include "BKE_idtype.h"
#include "BKE_image.h"
#include "BKE_main.h"
#include "BKE_mball_tessellate.h"
#include "BKE_modifier.h"
#include "BKE_node.h"
#include "BKE_scene.h"
#include "BKE_vfont.h"
#include "BLI_path_util.h"
#include "BLI_threads.h"
@ -44,6 +46,8 @@
#include "IMB_imbuf.h"
#include "ED_datafiles.h"
#include "RNA_define.h"
#include "WM_api.h"
@ -73,6 +77,7 @@ void BlendfileLoadingBaseTest::SetUpTestCase()
RNA_init();
BKE_node_system_init();
BKE_callback_global_init();
BKE_vfont_builtin_register(datatoc_bfont_pfb, datatoc_bfont_pfb_size);
G.background = true;
G.factory_startup = true;
@ -111,6 +116,7 @@ void BlendfileLoadingBaseTest::TearDownTestCase()
void BlendfileLoadingBaseTest::TearDown()
{
BKE_mball_cubeTable_free();
depsgraph_free();
blendfile_free();

View File

@ -25,6 +25,20 @@ set(INC
../../io/alembic
../../io/collada
../../io/gpencil
../../io/wavefront_obj
../../io/usd
../../makesdna
../../makesrna
../../windowmanager
../../../../intern/guardedalloc
)
set(INC_SYS
)
set(SRC
io_alembic.c
../../io/usd
../../makesdna
../../makesrna
@ -43,6 +57,7 @@ set(SRC
io_gpencil_export.c
io_gpencil_import.c
io_gpencil_utils.c
io_obj.c
io_ops.c
io_usd.c
@ -57,6 +72,7 @@ set(SRC
set(LIB
bf_blenkernel
bf_blenlib
bf_wavefront_obj
)
if(WITH_OPENCOLLADA)

View File

@ -0,0 +1,369 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup editor/io
*/
#include "DNA_space_types.h"
#include "BKE_context.h"
#include "BKE_main.h"
#include "BKE_report.h"
#include "BLI_path_util.h"
#include "BLI_string.h"
#include "BLI_utildefines.h"
#include "BLT_translation.h"
#include "MEM_guardedalloc.h"
#include "RNA_access.h"
#include "RNA_define.h"
#include "UI_interface.h"
#include "UI_resources.h"
#include "WM_api.h"
#include "WM_types.h"
#include "DEG_depsgraph.h"
#include "IO_wavefront_obj.h"
#include "io_obj.h"
const EnumPropertyItem io_obj_transform_axis_forward[] = {
{OBJ_AXIS_X_FORWARD, "X_FORWARD", 0, "X", "Positive X axis"},
{OBJ_AXIS_Y_FORWARD, "Y_FORWARD", 0, "Y", "Positive Y axis"},
{OBJ_AXIS_Z_FORWARD, "Z_FORWARD", 0, "Z", "Positive Z axis"},
{OBJ_AXIS_NEGATIVE_X_FORWARD, "NEGATIVE_X_FORWARD", 0, "-X", "Negative X axis"},
{OBJ_AXIS_NEGATIVE_Y_FORWARD, "NEGATIVE_Y_FORWARD", 0, "-Y", "Negative Y axis"},
{OBJ_AXIS_NEGATIVE_Z_FORWARD, "NEGATIVE_Z_FORWARD", 0, "-Z (Default)", "Negative Z axis"},
{0, NULL, 0, NULL, NULL}};
const EnumPropertyItem io_obj_transform_axis_up[] = {
{OBJ_AXIS_X_UP, "X_UP", 0, "X", "Positive X axis"},
{OBJ_AXIS_Y_UP, "Y_UP", 0, "Y (Default)", "Positive Y axis"},
{OBJ_AXIS_Z_UP, "Z_UP", 0, "Z", "Positive Z axis"},
{OBJ_AXIS_NEGATIVE_X_UP, "NEGATIVE_X_UP", 0, "-X", "Negative X axis"},
{OBJ_AXIS_NEGATIVE_Y_UP, "NEGATIVE_Y_UP", 0, "-Y", "Negative Y axis"},
{OBJ_AXIS_NEGATIVE_Z_UP, "NEGATIVE_Z_UP", 0, "-Z", "Negative Z axis"},
{0, NULL, 0, NULL, NULL}};
const EnumPropertyItem io_obj_export_evaluation_mode[] = {
{DAG_EVAL_RENDER, "DAG_EVAL_RENDER", 0, "Render", "Export objects as they appear in render"},
{DAG_EVAL_VIEWPORT,
"DAG_EVAL_VIEWPORT",
0,
"Viewport (Default)",
"Export objects as they appear in the viewport"},
{0, NULL, 0, NULL, NULL}};
static int wm_obj_export_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event))
{
if (!RNA_struct_property_is_set(op->ptr, "filepath")) {
Main *bmain = CTX_data_main(C);
char filepath[FILE_MAX];
if (BKE_main_blendfile_path(bmain)[0] == '\0') {
BLI_strncpy(filepath, "untitled", sizeof(filepath));
}
else {
BLI_strncpy(filepath, BKE_main_blendfile_path(bmain), sizeof(filepath));
}
BLI_path_extension_replace(filepath, sizeof(filepath), ".obj");
RNA_string_set(op->ptr, "filepath", filepath);
}
WM_event_add_fileselect(C, op);
return OPERATOR_RUNNING_MODAL;
}
static int wm_obj_export_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 OBJExportParams export_params;
RNA_string_get(op->ptr, "filepath", export_params.filepath);
export_params.blen_filepath = CTX_data_main(C)->filepath;
export_params.export_animation = RNA_boolean_get(op->ptr, "export_animation");
export_params.start_frame = RNA_int_get(op->ptr, "start_frame");
export_params.end_frame = RNA_int_get(op->ptr, "end_frame");
export_params.forward_axis = RNA_enum_get(op->ptr, "forward_axis");
export_params.up_axis = RNA_enum_get(op->ptr, "up_axis");
export_params.scaling_factor = RNA_float_get(op->ptr, "scaling_factor");
export_params.export_eval_mode = RNA_enum_get(op->ptr, "export_eval_mode");
export_params.export_selected_objects = RNA_boolean_get(op->ptr, "export_selected_objects");
export_params.export_uv = RNA_boolean_get(op->ptr, "export_uv");
export_params.export_normals = RNA_boolean_get(op->ptr, "export_normals");
export_params.export_materials = RNA_boolean_get(op->ptr, "export_materials");
export_params.export_triangulated_mesh = RNA_boolean_get(op->ptr, "export_triangulated_mesh");
export_params.export_curves_as_nurbs = RNA_boolean_get(op->ptr, "export_curves_as_nurbs");
export_params.export_object_groups = RNA_boolean_get(op->ptr, "export_object_groups");
export_params.export_material_groups = RNA_boolean_get(op->ptr, "export_material_groups");
export_params.export_vertex_groups = RNA_boolean_get(op->ptr, "export_vertex_groups");
export_params.export_smooth_groups = RNA_boolean_get(op->ptr, "export_smooth_groups");
export_params.smooth_groups_bitflags = RNA_boolean_get(op->ptr, "smooth_group_bitflags");
OBJ_export(C, &export_params);
return OPERATOR_FINISHED;
}
static void ui_obj_export_settings(uiLayout *layout, PointerRNA *imfptr)
{
const bool export_animation = RNA_boolean_get(imfptr, "export_animation");
const bool export_smooth_groups = RNA_boolean_get(imfptr, "export_smooth_groups");
uiLayoutSetPropSep(layout, true);
uiLayoutSetPropDecorate(layout, false);
/* Animation options. */
uiLayout *box = uiLayoutBox(layout);
uiItemL(box, IFACE_("Animation"), ICON_ANIM);
uiLayout *col = uiLayoutColumn(box, false);
uiLayout *sub = uiLayoutColumn(col, false);
uiItemR(sub, imfptr, "export_animation", 0, NULL, ICON_NONE);
sub = uiLayoutColumn(sub, true);
uiItemR(sub, imfptr, "start_frame", 0, IFACE_("Frame Start"), ICON_NONE);
uiItemR(sub, imfptr, "end_frame", 0, IFACE_("End"), ICON_NONE);
uiLayoutSetEnabled(sub, export_animation);
/* Object Transform options. */
box = uiLayoutBox(layout);
uiItemL(box, IFACE_("Object Properties"), ICON_OBJECT_DATA);
col = uiLayoutColumn(box, false);
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);
sub = uiLayoutColumn(col, false);
uiItemR(sub, imfptr, "scaling_factor", 0, NULL, ICON_NONE);
sub = uiLayoutColumnWithHeading(col, false, IFACE_("Objects"));
uiItemR(sub, imfptr, "export_selected_objects", 0, IFACE_("Selected Only"), ICON_NONE);
uiItemR(sub, imfptr, "export_eval_mode", 0, IFACE_("Properties"), ICON_NONE);
/* Options for what to write. */
box = uiLayoutBox(layout);
uiItemL(box, IFACE_("Geometry Export"), ICON_EXPORT);
col = uiLayoutColumn(box, false);
sub = uiLayoutColumnWithHeading(col, false, IFACE_("Export"));
uiItemR(sub, imfptr, "export_uv", 0, IFACE_("UV Coordinates"), ICON_NONE);
uiItemR(sub, imfptr, "export_normals", 0, IFACE_("Normals"), ICON_NONE);
uiItemR(sub, imfptr, "export_materials", 0, IFACE_("Materials"), ICON_NONE);
uiItemR(sub, imfptr, "export_triangulated_mesh", 0, IFACE_("Triangulated Mesh"), ICON_NONE);
uiItemR(sub, imfptr, "export_curves_as_nurbs", 0, IFACE_("Curves as NURBS"), ICON_NONE);
box = uiLayoutBox(layout);
uiItemL(box, IFACE_("Grouping"), ICON_GROUP);
col = uiLayoutColumn(box, false);
sub = uiLayoutColumnWithHeading(col, false, IFACE_("Export"));
uiItemR(sub, imfptr, "export_object_groups", 0, IFACE_("Object Groups"), ICON_NONE);
uiItemR(sub, imfptr, "export_material_groups", 0, IFACE_("Material Groups"), ICON_NONE);
uiItemR(sub, imfptr, "export_vertex_groups", 0, IFACE_("Vertex Groups"), ICON_NONE);
uiItemR(sub, imfptr, "export_smooth_groups", 0, IFACE_("Smooth Groups"), ICON_NONE);
sub = uiLayoutColumn(sub, false);
uiLayoutSetEnabled(sub, export_smooth_groups);
uiItemR(sub, imfptr, "smooth_group_bitflags", 0, IFACE_("Smooth Group Bitflags"), ICON_NONE);
}
static void wm_obj_export_draw(bContext *UNUSED(C), wmOperator *op)
{
PointerRNA ptr;
RNA_pointer_create(NULL, op->type->srna, op->properties, &ptr);
ui_obj_export_settings(op->layout, &ptr);
}
/**
* Return true if any property in the UI is changed.
*/
static bool wm_obj_export_check(bContext *C, wmOperator *op)
{
char filepath[FILE_MAX];
Scene *scene = CTX_data_scene(C);
bool changed = false;
RNA_string_get(op->ptr, "filepath", filepath);
if (!BLI_path_extension_check(filepath, ".obj")) {
BLI_path_extension_ensure(filepath, FILE_MAX, ".obj");
RNA_string_set(op->ptr, "filepath", filepath);
changed = true;
}
{
int start = RNA_int_get(op->ptr, "start_frame");
int end = RNA_int_get(op->ptr, "end_frame");
/* Set the defaults. */
if (start == INT_MIN) {
start = SFRA;
changed = true;
}
if (end == INT_MAX) {
end = EFRA;
changed = true;
}
/* Fix user errors. */
if (end < start) {
end = start;
changed = true;
}
RNA_int_set(op->ptr, "start_frame", start);
RNA_int_set(op->ptr, "end_frame", end);
}
/* Both forward and up axes cannot be the same (or same except opposite sign). */
if (RNA_enum_get(op->ptr, "forward_axis") % TOTAL_AXES ==
(RNA_enum_get(op->ptr, "up_axis") % TOTAL_AXES)) {
/* TODO (ankitm) Show a warning here. */
RNA_enum_set(op->ptr, "up_axis", RNA_enum_get(op->ptr, "up_axis") % TOTAL_AXES + 1);
changed = true;
}
return changed;
}
void WM_OT_obj_export(struct wmOperatorType *ot)
{
ot->name = "Export Wavefront OBJ";
ot->description = "Save the scene to a Wavefront OBJ file";
ot->idname = "WM_OT_obj_export";
ot->invoke = wm_obj_export_invoke;
ot->exec = wm_obj_export_exec;
ot->poll = WM_operator_winactive;
ot->ui = wm_obj_export_draw;
ot->check = wm_obj_export_check;
WM_operator_properties_filesel(ot,
FILE_TYPE_FOLDER | FILE_TYPE_OBJECT_IO,
FILE_BLENDER,
FILE_SAVE,
WM_FILESEL_FILEPATH | WM_FILESEL_SHOW_PROPS,
FILE_DEFAULTDISPLAY,
FILE_SORT_ALPHA);
/* Animation options. */
RNA_def_boolean(ot->srna,
"export_animation",
false,
"Export Animation",
"Export multiple frames instead of the current frame only");
RNA_def_int(ot->srna,
"start_frame",
INT_MIN, /* wm_obj_export_check uses this to set SFRA. */
INT_MIN,
INT_MAX,
"Start Frame",
"The first frame to be exported",
INT_MIN,
INT_MAX);
RNA_def_int(ot->srna,
"end_frame",
INT_MAX, /* wm_obj_export_check uses this to set EFRA. */
INT_MIN,
INT_MAX,
"End Frame",
"The last frame to be exported",
INT_MIN,
INT_MAX);
/* Object transform options. */
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", "");
RNA_def_float(ot->srna,
"scaling_factor",
1.0f,
0.001f,
10000.0f,
"Scale",
"Upscale the object by this factor",
0.01,
1000.0f);
/* File Writer options. */
RNA_def_enum(ot->srna,
"export_eval_mode",
io_obj_export_evaluation_mode,
DAG_EVAL_VIEWPORT,
"Object Properties",
"Determines properties like object visibility, modifiers etc., where they differ "
"for Render and Viewport");
RNA_def_boolean(ot->srna,
"export_selected_objects",
false,
"Export Selected Objects",
"Export only selected objects instead of all supported objects");
RNA_def_boolean(ot->srna, "export_uv", true, "Export UVs", "");
RNA_def_boolean(ot->srna,
"export_normals",
true,
"Export Normals",
"Export per-face normals if the face is flat-shaded, per-face-per-loop "
"normals if smooth-shaded");
RNA_def_boolean(ot->srna,
"export_materials",
true,
"Export Materials",
"Export MTL library. There must be a Principled-BSDF node for image textures to "
"be exported to the MTL file");
RNA_def_boolean(ot->srna,
"export_triangulated_mesh",
false,
"Export Triangulated Mesh",
"All ngons with four or more vertices will be triangulated. Meshes in "
"the scene will not be affected. Behaves like Triangulate Modifier with "
"ngon-method: \"Beauty\", quad-method: \"Shortest Diagonal\", min vertices: 4");
RNA_def_boolean(ot->srna,
"export_curves_as_nurbs",
false,
"Export Curves as NURBS",
"Export curves in parametric form instead of exporting as mesh");
RNA_def_boolean(ot->srna,
"export_object_groups",
false,
"Export Object Groups",
"Append mesh name to object name, separated by a '_'");
RNA_def_boolean(ot->srna,
"export_material_groups",
false,
"Export Material Groups",
"Append mesh name and material name to object name, separated by a '_'");
RNA_def_boolean(
ot->srna,
"export_vertex_groups",
false,
"Export Vertex Groups",
"Export the name of the vertex group of a face. It is approximated "
"by choosing the vertex group with the most members among the vertices of a face");
RNA_def_boolean(
ot->srna,
"export_smooth_groups",
false,
"Export Smooth Groups",
"Every smooth-shaded face is assigned group \"1\" and every flat-shaded face \"off\"");
RNA_def_boolean(
ot->srna, "smooth_group_bitflags", false, "Generate Bitflags for Smooth Groups", "");
}

View File

@ -0,0 +1,25 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup editor/io
*/
#pragma once
struct wmOperatorType;
void WM_OT_obj_export(struct wmOperatorType *ot);

View File

@ -39,6 +39,7 @@
#include "io_cache.h"
#include "io_gpencil.h"
#include "io_obj.h"
void ED_operatortypes_io(void)
{
@ -68,4 +69,5 @@ void ED_operatortypes_io(void)
WM_operatortype_append(CACHEFILE_OT_open);
WM_operatortype_append(CACHEFILE_OT_reload);
WM_operatortype_append(WM_OT_obj_export);
}

View File

@ -275,6 +275,9 @@ static FileSelectParams *fileselect_ensure_updated_file_params(SpaceFile *sfile)
if ((prop = RNA_struct_find_property(op->ptr, "filter_usd"))) {
params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_USD : 0;
}
if ((prop = RNA_struct_find_property(op->ptr, "filter_obj"))) {
params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_OBJECT_IO : 0;
}
if ((prop = RNA_struct_find_property(op->ptr, "filter_volume"))) {
params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_VOLUME : 0;
}

View File

@ -19,6 +19,7 @@
# ***** END GPL LICENSE BLOCK *****
add_subdirectory(common)
add_subdirectory(wavefront_obj)
if(WITH_ALEMBIC)
add_subdirectory(alembic)

View File

@ -0,0 +1,84 @@
# ***** BEGIN GPL LICENSE BLOCK *****
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# ***** END GPL LICENSE BLOCK *****
set(INC
.
./exporter
../../blenkernel
../../blenlib
../../bmesh
../../bmesh/intern
../../depsgraph
../../editors/include
../../makesdna
../../makesrna
../../nodes
../../windowmanager
../../../../intern/guardedalloc
)
set(INC_SYS
)
set(SRC
IO_wavefront_obj.cc
exporter/obj_exporter.cc
exporter/obj_export_file_writer.cc
exporter/obj_export_mesh.cc
exporter/obj_export_mtl.cc
exporter/obj_export_nurbs.cc
IO_wavefront_obj.h
exporter/obj_exporter.hh
exporter/obj_export_file_writer.hh
exporter/obj_export_io.hh
exporter/obj_export_mesh.hh
exporter/obj_export_mtl.hh
exporter/obj_export_nurbs.hh
)
set(LIB
bf_blenkernel
)
blender_add_lib(bf_wavefront_obj "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
if(WITH_GTESTS)
set(TEST_SRC
tests/obj_exporter_tests.cc
tests/obj_exporter_tests.hh
)
set(TEST_INC
${INC}
../../blenloader
../../../../tests/gtests
)
set(TEST_LIB
${LIB}
bf_blenloader_tests
bf_wavefront_obj
)
include(GTestTesting)
blender_add_test_lib(bf_wavefront_obj_tests "${TEST_SRC}" "${TEST_INC}" "${INC_SYS}" "${TEST_LIB}")
add_dependencies(bf_wavefront_obj_tests bf_wavefront_obj)
endif()

View File

@ -0,0 +1,34 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup obj
*/
#include "BLI_timeit.hh"
#include "IO_wavefront_obj.h"
#include "obj_exporter.hh"
/**
* C-interface for the exporter.
*/
void OBJ_export(bContext *C, const OBJExportParams *export_params)
{
SCOPED_TIMER("OBJ export");
blender::io::obj::exporter_main(C, *export_params);
}

View File

@ -0,0 +1,97 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup obj
*/
#pragma once
#include "BKE_context.h"
#include "BLI_path_util.h"
#include "DEG_depsgraph.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
OBJ_AXIS_X_UP = 0,
OBJ_AXIS_Y_UP = 1,
OBJ_AXIS_Z_UP = 2,
OBJ_AXIS_NEGATIVE_X_UP = 3,
OBJ_AXIS_NEGATIVE_Y_UP = 4,
OBJ_AXIS_NEGATIVE_Z_UP = 5,
} eTransformAxisUp;
typedef enum {
OBJ_AXIS_X_FORWARD = 0,
OBJ_AXIS_Y_FORWARD = 1,
OBJ_AXIS_Z_FORWARD = 2,
OBJ_AXIS_NEGATIVE_X_FORWARD = 3,
OBJ_AXIS_NEGATIVE_Y_FORWARD = 4,
OBJ_AXIS_NEGATIVE_Z_FORWARD = 5,
} eTransformAxisForward;
const int TOTAL_AXES = 3;
struct OBJExportParams {
/** Full path to the destination .OBJ file. */
char filepath[FILE_MAX];
/** Full path to current blender file (used for comments in output). */
const char *blen_filepath;
/** Whether multiple frames should be exported. */
bool export_animation;
/** The first frame to be exported. */
int start_frame;
/** The last frame to be exported. */
int end_frame;
/* Geometry Transform options. */
eTransformAxisForward forward_axis;
eTransformAxisUp up_axis;
float scaling_factor;
/* File Write Options. */
bool export_selected_objects;
eEvaluationMode export_eval_mode;
bool export_uv;
bool export_normals;
bool export_materials;
bool export_triangulated_mesh;
bool export_curves_as_nurbs;
/* Grouping options. */
bool export_object_groups;
bool export_material_groups;
bool export_vertex_groups;
/**
* Calculate smooth groups from sharp edges.
*/
bool export_smooth_groups;
/**
* Create bitflags instead of the default "0"/"1" group IDs.
*/
bool smooth_groups_bitflags;
};
void OBJ_export(bContext *C, const struct OBJExportParams *export_params);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,626 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup obj
*/
#include <algorithm>
#include <cstdio>
#include "BKE_blender_version.h"
#include "BLI_path_util.h"
#include "obj_export_mesh.hh"
#include "obj_export_mtl.hh"
#include "obj_export_nurbs.hh"
#include "obj_export_file_writer.hh"
namespace blender::io::obj {
/**
* Per reference http://www.martinreddy.net/gfx/3d/OBJ.spec:
* To turn off smoothing groups, use a value of 0 or off.
* Polygonal elements use group numbers to put elements in different smoothing groups.
* For free-form surfaces, smoothing groups are either turned on or off;
* there is no difference between values greater than 0.
*/
const int SMOOTH_GROUP_DISABLED = 0;
const int SMOOTH_GROUP_DEFAULT = 1;
const char *DEFORM_GROUP_DISABLED = "off";
/* There is no deform group default name. Use what the user set in the UI. */
/**
* Per reference http://www.martinreddy.net/gfx/3d/OBJ.spec:
* Once a material is assigned, it cannot be turned off; it can only be changed.
* If a material name is not specified, a white material is used.
* So an empty material name is written. */
const char *MATERIAL_GROUP_DISABLED = "";
/**
* Write one line of polygon indices as "f v1/vt1/vn1 v2/vt2/vn2 ...".
*/
void OBJWriter::write_vert_uv_normal_indices(Span<int> vert_indices,
Span<int> uv_indices,
Span<int> normal_indices) const
{
BLI_assert(vert_indices.size() == uv_indices.size() &&
vert_indices.size() == normal_indices.size());
file_handler_->write<eOBJSyntaxElement::poly_element_begin>();
for (int j = 0; j < vert_indices.size(); j++) {
file_handler_->write<eOBJSyntaxElement::vertex_uv_normal_indices>(
vert_indices[j] + index_offsets_.vertex_offset + 1,
uv_indices[j] + index_offsets_.uv_vertex_offset + 1,
normal_indices[j] + index_offsets_.normal_offset + 1);
}
file_handler_->write<eOBJSyntaxElement::poly_element_end>();
}
/**
* Write one line of polygon indices as "f v1//vn1 v2//vn2 ...".
*/
void OBJWriter::write_vert_normal_indices(Span<int> vert_indices,
Span<int> /*uv_indices*/,
Span<int> normal_indices) const
{
BLI_assert(vert_indices.size() == normal_indices.size());
file_handler_->write<eOBJSyntaxElement::poly_element_begin>();
for (int j = 0; j < vert_indices.size(); j++) {
file_handler_->write<eOBJSyntaxElement::vertex_normal_indices>(
vert_indices[j] + index_offsets_.vertex_offset + 1,
normal_indices[j] + index_offsets_.normal_offset + 1);
}
file_handler_->write<eOBJSyntaxElement::poly_element_end>();
}
/**
* Write one line of polygon indices as "f v1/vt1 v2/vt2 ...".
*/
void OBJWriter::write_vert_uv_indices(Span<int> vert_indices,
Span<int> uv_indices,
Span<int> /*normal_indices*/) const
{
BLI_assert(vert_indices.size() == uv_indices.size());
file_handler_->write<eOBJSyntaxElement::poly_element_begin>();
for (int j = 0; j < vert_indices.size(); j++) {
file_handler_->write<eOBJSyntaxElement::vertex_uv_indices>(
vert_indices[j] + index_offsets_.vertex_offset + 1,
uv_indices[j] + index_offsets_.uv_vertex_offset + 1);
}
file_handler_->write<eOBJSyntaxElement::poly_element_end>();
}
/**
* Write one line of polygon indices as "f v1 v2 ...".
*/
void OBJWriter::write_vert_indices(Span<int> vert_indices,
Span<int> /*uv_indices*/,
Span<int> /*normal_indices*/) const
{
file_handler_->write<eOBJSyntaxElement::poly_element_begin>();
for (const int vert_index : vert_indices) {
file_handler_->write<eOBJSyntaxElement::vertex_indices>(vert_index +
index_offsets_.vertex_offset + 1);
}
file_handler_->write<eOBJSyntaxElement::poly_element_end>();
}
void OBJWriter::write_header() const
{
using namespace std::string_literals;
file_handler_->write<eOBJSyntaxElement::string>("# Blender "s + BKE_blender_version_string() +
"\n");
file_handler_->write<eOBJSyntaxElement::string>("# www.blender.org\n");
}
/**
* Write file name of Material Library in .OBJ file.
*/
void OBJWriter::write_mtllib_name(const StringRefNull mtl_filepath) const
{
/* Split .MTL file path into parent directory and filename. */
char mtl_file_name[FILE_MAXFILE];
char mtl_dir_name[FILE_MAXDIR];
BLI_split_dirfile(mtl_filepath.data(), mtl_dir_name, mtl_file_name, FILE_MAXDIR, FILE_MAXFILE);
file_handler_->write<eOBJSyntaxElement::mtllib>(mtl_file_name);
}
/**
* Write an object's group with mesh and/or material name appended conditionally.
*/
void OBJWriter::write_object_group(const OBJMesh &obj_mesh_data) const
{
/* "o object_name" is not mandatory. A valid .OBJ file may contain neither
* "o name" nor "g group_name". */
BLI_assert(export_params_.export_object_groups);
if (!export_params_.export_object_groups) {
return;
}
const std::string object_name = obj_mesh_data.get_object_name();
const char *object_mesh_name = obj_mesh_data.get_object_mesh_name();
const char *object_material_name = obj_mesh_data.get_object_material_name(0);
if (export_params_.export_materials && export_params_.export_material_groups &&
object_material_name) {
file_handler_->write<eOBJSyntaxElement::object_group>(object_name + "_" + object_mesh_name +
"_" + object_material_name);
return;
}
file_handler_->write<eOBJSyntaxElement::object_group>(object_name + "_" + object_mesh_name);
}
/**
* Write object's name or group.
*/
void OBJWriter::write_object_name(const OBJMesh &obj_mesh_data) const
{
const char *object_name = obj_mesh_data.get_object_name();
if (export_params_.export_object_groups) {
write_object_group(obj_mesh_data);
return;
}
file_handler_->write<eOBJSyntaxElement::object_name>(object_name);
}
/**
* Write vertex coordinates for all vertices as "v x y z".
*/
void OBJWriter::write_vertex_coords(const OBJMesh &obj_mesh_data) const
{
const int tot_vertices = obj_mesh_data.tot_vertices();
for (int i = 0; i < tot_vertices; i++) {
float3 vertex = obj_mesh_data.calc_vertex_coords(i, export_params_.scaling_factor);
file_handler_->write<eOBJSyntaxElement::vertex_coords>(vertex[0], vertex[1], vertex[2]);
}
}
/**
* Write UV vertex coordinates for all vertices as "vt u v".
* \note UV indices are stored here, but written later.
*/
void OBJWriter::write_uv_coords(OBJMesh &r_obj_mesh_data) const
{
Vector<std::array<float, 2>> uv_coords;
/* UV indices are calculated and stored in an OBJMesh member here. */
r_obj_mesh_data.store_uv_coords_and_indices(uv_coords);
for (const std::array<float, 2> &uv_vertex : uv_coords) {
file_handler_->write<eOBJSyntaxElement::uv_vertex_coords>(uv_vertex[0], uv_vertex[1]);
}
}
/**
* Write loop normals for smooth-shaded polygons, and polygon normals otherwise, as "vn x y z".
*/
void OBJWriter::write_poly_normals(const OBJMesh &obj_mesh_data) const
{
obj_mesh_data.ensure_mesh_normals();
Vector<float3> lnormals;
const int tot_polygons = obj_mesh_data.tot_polygons();
for (int i = 0; i < tot_polygons; i++) {
if (obj_mesh_data.is_ith_poly_smooth(i)) {
obj_mesh_data.calc_loop_normals(i, lnormals);
for (const float3 &lnormal : lnormals) {
file_handler_->write<eOBJSyntaxElement::normal>(lnormal[0], lnormal[1], lnormal[2]);
}
}
else {
float3 poly_normal = obj_mesh_data.calc_poly_normal(i);
file_handler_->write<eOBJSyntaxElement::normal>(
poly_normal[0], poly_normal[1], poly_normal[2]);
}
}
}
/**
* Write smooth group if polygon at the given index is shaded smooth else "s 0"
*/
int OBJWriter::write_smooth_group(const OBJMesh &obj_mesh_data,
const int poly_index,
const int last_poly_smooth_group) const
{
int current_group = SMOOTH_GROUP_DISABLED;
if (!export_params_.export_smooth_groups && obj_mesh_data.is_ith_poly_smooth(poly_index)) {
/* Smooth group calculation is disabled, but polygon is smooth-shaded. */
current_group = SMOOTH_GROUP_DEFAULT;
}
else if (obj_mesh_data.is_ith_poly_smooth(poly_index)) {
/* Smooth group calc is enabled and polygon is smoothshaded, so find the group. */
current_group = obj_mesh_data.ith_smooth_group(poly_index);
}
if (current_group == last_poly_smooth_group) {
/* Group has already been written, even if it is "s 0". */
return current_group;
}
file_handler_->write<eOBJSyntaxElement::smooth_group>(current_group);
return current_group;
}
/**
* Write material name and material group of a polygon in the .OBJ file.
* \return #mat_nr of the polygon at the given index.
* \note It doesn't write to the material library.
*/
int16_t OBJWriter::write_poly_material(const OBJMesh &obj_mesh_data,
const int poly_index,
const int16_t last_poly_mat_nr,
std::function<const char *(int)> matname_fn) const
{
if (!export_params_.export_materials || obj_mesh_data.tot_materials() <= 0) {
return last_poly_mat_nr;
}
const int16_t current_mat_nr = obj_mesh_data.ith_poly_matnr(poly_index);
/* Whenever a polygon with a new material is encountered, write its material
* and/or group, otherwise pass. */
if (last_poly_mat_nr == current_mat_nr) {
return current_mat_nr;
}
if (current_mat_nr == NOT_FOUND) {
file_handler_->write<eOBJSyntaxElement::poly_usemtl>(MATERIAL_GROUP_DISABLED);
return current_mat_nr;
}
if (export_params_.export_object_groups) {
write_object_group(obj_mesh_data);
}
const char *mat_name = matname_fn(current_mat_nr);
if (!mat_name) {
mat_name = MATERIAL_GROUP_DISABLED;
}
file_handler_->write<eOBJSyntaxElement::poly_usemtl>(mat_name);
return current_mat_nr;
}
/**
* Write the name of the deform group of a polygon.
*/
int16_t OBJWriter::write_vertex_group(const OBJMesh &obj_mesh_data,
const int poly_index,
const int16_t last_poly_vertex_group) const
{
if (!export_params_.export_vertex_groups) {
return last_poly_vertex_group;
}
const int16_t current_group = obj_mesh_data.get_poly_deform_group_index(poly_index);
if (current_group == last_poly_vertex_group) {
/* No vertex group found in this polygon, just like in the last iteration. */
return current_group;
}
if (current_group == NOT_FOUND) {
file_handler_->write<eOBJSyntaxElement::object_group>(DEFORM_GROUP_DISABLED);
return current_group;
}
file_handler_->write<eOBJSyntaxElement::object_group>(
obj_mesh_data.get_poly_deform_group_name(current_group));
return current_group;
}
/**
* \return Writer function with appropriate polygon-element syntax.
*/
OBJWriter::func_vert_uv_normal_indices OBJWriter::get_poly_element_writer(
const int total_uv_vertices) const
{
if (export_params_.export_normals) {
if (export_params_.export_uv && (total_uv_vertices > 0)) {
/* Write both normals and UV indices. */
return &OBJWriter::write_vert_uv_normal_indices;
}
/* Write normals indices. */
return &OBJWriter::write_vert_normal_indices;
}
/* Write UV indices. */
if (export_params_.export_uv && (total_uv_vertices > 0)) {
return &OBJWriter::write_vert_uv_indices;
}
/* Write neither normals nor UV indices. */
return &OBJWriter::write_vert_indices;
}
/**
* Write polygon elements with at least vertex indices, and conditionally with UV vertex
* indices and polygon normal indices. Also write groups: smooth, vertex, material.
* The matname_fn turns a 0-indexed material slot number in an Object into the
* name used in the .obj file.
* \note UV indices were stored while writing UV vertices.
*/
void OBJWriter::write_poly_elements(const OBJMesh &obj_mesh_data,
std::function<const char *(int)> matname_fn)
{
int last_poly_smooth_group = NEGATIVE_INIT;
int16_t last_poly_vertex_group = NEGATIVE_INIT;
int16_t last_poly_mat_nr = NEGATIVE_INIT;
const func_vert_uv_normal_indices poly_element_writer = get_poly_element_writer(
obj_mesh_data.tot_uv_vertices());
/* Number of normals may not be equal to number of polygons due to smooth shading. */
int per_object_tot_normals = 0;
const int tot_polygons = obj_mesh_data.tot_polygons();
for (int i = 0; i < tot_polygons; i++) {
Vector<int> poly_vertex_indices = obj_mesh_data.calc_poly_vertex_indices(i);
Span<int> poly_uv_indices = obj_mesh_data.calc_poly_uv_indices(i);
/* For an Object, a normal index depends on how many of its normals have been written before
* it. This is unknown because of smooth shading. So pass "per object total normals"
* and update it after each call. */
int new_normals = 0;
Vector<int> poly_normal_indices;
std::tie(new_normals, poly_normal_indices) = obj_mesh_data.calc_poly_normal_indices(
i, per_object_tot_normals);
per_object_tot_normals += new_normals;
last_poly_smooth_group = write_smooth_group(obj_mesh_data, i, last_poly_smooth_group);
last_poly_vertex_group = write_vertex_group(obj_mesh_data, i, last_poly_vertex_group);
last_poly_mat_nr = write_poly_material(obj_mesh_data, i, last_poly_mat_nr, matname_fn);
(this->*poly_element_writer)(poly_vertex_indices, poly_uv_indices, poly_normal_indices);
}
/* Unusual: Other indices are updated in #OBJWriter::update_index_offsets. */
index_offsets_.normal_offset += per_object_tot_normals;
}
/**
* Write loose edges of a mesh as "l v1 v2".
*/
void OBJWriter::write_edges_indices(const OBJMesh &obj_mesh_data) const
{
obj_mesh_data.ensure_mesh_edges();
const int tot_edges = obj_mesh_data.tot_edges();
for (int edge_index = 0; edge_index < tot_edges; edge_index++) {
const std::optional<std::array<int, 2>> vertex_indices =
obj_mesh_data.calc_loose_edge_vert_indices(edge_index);
if (!vertex_indices) {
continue;
}
file_handler_->write<eOBJSyntaxElement::edge>(
(*vertex_indices)[0] + index_offsets_.vertex_offset + 1,
(*vertex_indices)[1] + index_offsets_.vertex_offset + 1);
}
}
/**
* Write a NURBS curve to the .OBJ file in parameter form.
*/
void OBJWriter::write_nurbs_curve(const OBJCurve &obj_nurbs_data) const
{
const int total_splines = obj_nurbs_data.total_splines();
for (int spline_idx = 0; spline_idx < total_splines; spline_idx++) {
const int total_vertices = obj_nurbs_data.total_spline_vertices(spline_idx);
for (int vertex_idx = 0; vertex_idx < total_vertices; vertex_idx++) {
const float3 vertex_coords = obj_nurbs_data.vertex_coordinates(
spline_idx, vertex_idx, export_params_.scaling_factor);
file_handler_->write<eOBJSyntaxElement::vertex_coords>(
vertex_coords[0], vertex_coords[1], vertex_coords[2]);
}
const char *nurbs_name = obj_nurbs_data.get_curve_name();
const int nurbs_degree = obj_nurbs_data.get_nurbs_degree(spline_idx);
file_handler_->write<eOBJSyntaxElement::object_group>(nurbs_name);
file_handler_->write<eOBJSyntaxElement::cstype>();
file_handler_->write<eOBJSyntaxElement::nurbs_degree>(nurbs_degree);
/**
* The numbers written here are indices into the vertex coordinates written
* earlier, relative to the line that is going to be written.
* [0.0 - 1.0] is the curve parameter range.
* 0.0 1.0 -1 -2 -3 -4 for a non-cyclic curve with 4 vertices.
* 0.0 1.0 -1 -2 -3 -4 -1 -2 -3 for a cyclic curve with 4 vertices.
*/
const int total_control_points = obj_nurbs_data.total_spline_control_points(spline_idx);
file_handler_->write<eOBJSyntaxElement::curve_element_begin>();
for (int i = 0; i < total_control_points; i++) {
/* "+1" to keep indices one-based, even if they're negative: i.e., -1 refers to the
* last vertex coordinate, -2 second last. */
file_handler_->write<eOBJSyntaxElement::vertex_indices>(-((i % total_vertices) + 1));
}
file_handler_->write<eOBJSyntaxElement::curve_element_end>();
/**
* In "parm u 0 0.1 .." line:, (total control points + 2) equidistant numbers in the
* parameter range are inserted.
*/
file_handler_->write<eOBJSyntaxElement::nurbs_parameter_begin>();
for (int i = 1; i <= total_control_points + 2; i++) {
file_handler_->write<eOBJSyntaxElement::nurbs_parameters>(1.0f * i /
(total_control_points + 2 + 1));
}
file_handler_->write<eOBJSyntaxElement::nurbs_parameter_end>();
file_handler_->write<eOBJSyntaxElement::nurbs_group_end>();
}
}
/**
* When there are multiple objects in a frame, the indices of previous objects' coordinates or
* normals add up.
*/
void OBJWriter::update_index_offsets(const OBJMesh &obj_mesh_data)
{
index_offsets_.vertex_offset += obj_mesh_data.tot_vertices();
index_offsets_.uv_vertex_offset += obj_mesh_data.tot_uv_vertices();
/* Normal index is updated right after writing the normals. */
}
/* -------------------------------------------------------------------- */
/** \name .MTL writers.
* \{ */
/**
* Convert #float3 to string of space-separated numbers, with no leading or trailing space.
* Only to be used in NON-performance-critical code.
*/
static std::string float3_to_string(const float3 &numbers)
{
std::ostringstream r_string;
r_string << numbers[0] << " " << numbers[1] << " " << numbers[2];
return r_string.str();
};
/*
* Create the .MTL file.
*/
MTLWriter::MTLWriter(const char *obj_filepath) noexcept(false)
{
mtl_filepath_ = obj_filepath;
const bool ok = BLI_path_extension_replace(mtl_filepath_.data(), FILE_MAX, ".mtl");
if (!ok) {
throw std::system_error(ENAMETOOLONG, std::system_category(), "");
}
file_handler_ = std::make_unique<FileHandler<eFileType::MTL>>(mtl_filepath_);
}
void MTLWriter::write_header(const char *blen_filepath) const
{
using namespace std::string_literals;
const char *blen_basename = (blen_filepath && blen_filepath[0] != '\0') ?
BLI_path_basename(blen_filepath) :
"None";
file_handler_->write<eMTLSyntaxElement::string>("# Blender "s + BKE_blender_version_string() +
" MTL File: '" + blen_basename + "'\n");
file_handler_->write<eMTLSyntaxElement::string>("# www.blender.org\n");
}
StringRefNull MTLWriter::mtl_file_path() const
{
return mtl_filepath_;
}
/**
* Write properties sourced from p-BSDF node or #Object.Material.
*/
void MTLWriter::write_bsdf_properties(const MTLMaterial &mtl_material)
{
file_handler_->write<eMTLSyntaxElement::Ns>(mtl_material.Ns);
file_handler_->write<eMTLSyntaxElement::Ka>(
mtl_material.Ka.x, mtl_material.Ka.y, mtl_material.Ka.z);
file_handler_->write<eMTLSyntaxElement::Kd>(
mtl_material.Kd.x, mtl_material.Kd.y, mtl_material.Kd.z);
file_handler_->write<eMTLSyntaxElement::Ks>(
mtl_material.Ks.x, mtl_material.Ks.y, mtl_material.Ks.z);
file_handler_->write<eMTLSyntaxElement::Ke>(
mtl_material.Ke.x, mtl_material.Ke.y, mtl_material.Ke.z);
file_handler_->write<eMTLSyntaxElement::Ni>(mtl_material.Ni);
file_handler_->write<eMTLSyntaxElement::d>(mtl_material.d);
file_handler_->write<eMTLSyntaxElement::illum>(mtl_material.illum);
}
/**
* Write a texture map in the form "map_XX -s 1. 1. 1. -o 0. 0. 0. [-bm 1.] path/to/image".
*/
void MTLWriter::write_texture_map(
const MTLMaterial &mtl_material,
const Map<const eMTLSyntaxElement, tex_map_XX>::Item &texture_map)
{
std::string translation;
std::string scale;
std::string map_bump_strength;
/* Optional strings should have their own leading spaces. */
if (texture_map.value.translation != float3{0.0f, 0.0f, 0.0f}) {
translation.append(" -s ").append(float3_to_string(texture_map.value.translation));
}
if (texture_map.value.scale != float3{1.0f, 1.0f, 1.0f}) {
scale.append(" -o ").append(float3_to_string(texture_map.value.scale));
}
if (texture_map.key == eMTLSyntaxElement::map_Bump && mtl_material.map_Bump_strength > 0.0001f) {
map_bump_strength.append(" -bm ").append(std::to_string(mtl_material.map_Bump_strength));
}
#define SYNTAX_DISPATCH(eMTLSyntaxElement) \
if (texture_map.key == eMTLSyntaxElement) { \
file_handler_->write<eMTLSyntaxElement>(translation + scale + map_bump_strength, \
texture_map.value.image_path); \
return; \
}
SYNTAX_DISPATCH(eMTLSyntaxElement::map_Kd);
SYNTAX_DISPATCH(eMTLSyntaxElement::map_Ks);
SYNTAX_DISPATCH(eMTLSyntaxElement::map_Ns);
SYNTAX_DISPATCH(eMTLSyntaxElement::map_d);
SYNTAX_DISPATCH(eMTLSyntaxElement::map_refl);
SYNTAX_DISPATCH(eMTLSyntaxElement::map_Ke);
SYNTAX_DISPATCH(eMTLSyntaxElement::map_Bump);
BLI_assert(!"This map type was not written to the file.");
}
/**
* Write all of the material specifications to the MTL file.
* For consistency of output from run to run (useful for testing),
* the materials are sorted by name before writing.
*/
void MTLWriter::write_materials()
{
if (mtlmaterials_.size() == 0) {
return;
}
std::sort(mtlmaterials_.begin(),
mtlmaterials_.end(),
[](const MTLMaterial &a, const MTLMaterial &b) { return a.name < b.name; });
for (const MTLMaterial &mtlmat : mtlmaterials_) {
file_handler_->write<eMTLSyntaxElement::string>("\n");
file_handler_->write<eMTLSyntaxElement::newmtl>(mtlmat.name);
write_bsdf_properties(mtlmat);
for (const Map<const eMTLSyntaxElement, tex_map_XX>::Item &texture_map :
mtlmat.texture_maps.items()) {
if (!texture_map.value.image_path.empty()) {
write_texture_map(mtlmat, texture_map);
}
}
}
}
/**
* Add the materials of the given object to MTLWriter, deduping
* against ones that are already there.
* Return a Vector of indices into mtlmaterials_ that hold the MTLMaterial
* that corresponds to each material slot, in order, of the given Object.
* Indexes are returned rather than pointers to the MTLMaterials themselves
* because the mtlmaterials_ Vector may move around when resized.
*/
Vector<int> MTLWriter::add_materials(const OBJMesh &mesh_to_export)
{
Vector<int> r_mtl_indices;
r_mtl_indices.resize(mesh_to_export.tot_materials());
for (int16_t i = 0; i < mesh_to_export.tot_materials(); i++) {
const Material *material = mesh_to_export.get_object_material(i);
if (!material) {
r_mtl_indices[i] = -1;
continue;
}
int mtlmat_index = material_map_.lookup_default(material, -1);
if (mtlmat_index != -1) {
r_mtl_indices[i] = mtlmat_index;
}
else {
mtlmaterials_.append(mtlmaterial_for_material(material));
r_mtl_indices[i] = mtlmaterials_.size() - 1;
material_map_.add_new(material, r_mtl_indices[i]);
}
}
return r_mtl_indices;
}
const char *MTLWriter::mtlmaterial_name(int index)
{
if (index < 0 || index >= mtlmaterials_.size()) {
return nullptr;
}
return mtlmaterials_[index].name.c_str();
}
/** \} */
} // namespace blender::io::obj

View File

@ -0,0 +1,132 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup obj
*/
#pragma once
#include "DNA_meshdata_types.h"
#include "BLI_map.hh"
#include "BLI_vector.hh"
#include "IO_wavefront_obj.h"
#include "obj_export_io.hh"
#include "obj_export_mtl.hh"
namespace blender::io::obj {
class OBJCurve;
class OBJMesh;
/**
* Total vertices/ UV vertices/ normals of previous Objects
* should be added to the current Object's indices.
*/
struct IndexOffsets {
int vertex_offset;
int uv_vertex_offset;
int normal_offset;
};
/**
* Responsible for writing a .OBJ file.
*/
class OBJWriter : NonMovable, NonCopyable {
private:
const OBJExportParams &export_params_;
std::unique_ptr<FileHandler<eFileType::OBJ>> file_handler_ = nullptr;
IndexOffsets index_offsets_{0, 0, 0};
public:
OBJWriter(const char *filepath, const OBJExportParams &export_params) noexcept(false)
: export_params_(export_params)
{
file_handler_ = std::make_unique<FileHandler<eFileType::OBJ>>(filepath);
}
void write_header() const;
void write_object_name(const OBJMesh &obj_mesh_data) const;
void write_object_group(const OBJMesh &obj_mesh_data) const;
void write_mtllib_name(const StringRefNull mtl_filepath) const;
void write_vertex_coords(const OBJMesh &obj_mesh_data) const;
void write_uv_coords(OBJMesh &obj_mesh_data) const;
void write_poly_normals(const OBJMesh &obj_mesh_data) const;
int write_smooth_group(const OBJMesh &obj_mesh_data,
int poly_index,
const int last_poly_smooth_group) const;
int16_t write_poly_material(const OBJMesh &obj_mesh_data,
const int poly_index,
const int16_t last_poly_mat_nr,
std::function<const char *(int)> matname_fn) const;
int16_t write_vertex_group(const OBJMesh &obj_mesh_data,
const int poly_index,
const int16_t last_poly_vertex_group) const;
void write_poly_elements(const OBJMesh &obj_mesh_data,
std::function<const char *(int)> matname_fn);
void write_edges_indices(const OBJMesh &obj_mesh_data) const;
void write_nurbs_curve(const OBJCurve &obj_nurbs_data) const;
void update_index_offsets(const OBJMesh &obj_mesh_data);
private:
using func_vert_uv_normal_indices = void (OBJWriter::*)(Span<int> vert_indices,
Span<int> uv_indices,
Span<int> normal_indices) const;
func_vert_uv_normal_indices get_poly_element_writer(const int total_uv_vertices) const;
void write_vert_uv_normal_indices(Span<int> vert_indices,
Span<int> uv_indices,
Span<int> normal_indices) const;
void write_vert_normal_indices(Span<int> vert_indices,
Span<int> /*uv_indices*/,
Span<int> normal_indices) const;
void write_vert_uv_indices(Span<int> vert_indices,
Span<int> uv_indices,
Span<int> /*normal_indices*/) const;
void write_vert_indices(Span<int> vert_indices,
Span<int> /*uv_indices*/,
Span<int> /*normal_indices*/) const;
};
/**
* Responsible for writing a .MTL file.
*/
class MTLWriter : NonMovable, NonCopyable {
private:
std::unique_ptr<FileHandler<eFileType::MTL>> file_handler_ = nullptr;
std::string mtl_filepath_;
Vector<MTLMaterial> mtlmaterials_;
/* Map from a Material* to an index into mtlmaterials_. */
Map<const Material *, int> material_map_;
public:
MTLWriter(const char *obj_filepath) noexcept(false);
void write_header(const char *blen_filepath) const;
void write_materials();
StringRefNull mtl_file_path() const;
Vector<int> add_materials(const OBJMesh &mesh_to_export);
const char *mtlmaterial_name(int index);
private:
void write_bsdf_properties(const MTLMaterial &mtl_material);
void write_texture_map(const MTLMaterial &mtl_material,
const Map<const eMTLSyntaxElement, tex_map_XX>::Item &texture_map);
};
} // namespace blender::io::obj

View File

@ -0,0 +1,340 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup obj
*/
#pragma once
#include <cstdio>
#include <string>
#include <system_error>
#include <type_traits>
#include "BLI_compiler_attrs.h"
#include "BLI_string_ref.hh"
#include "BLI_utility_mixins.hh"
namespace blender::io::obj {
enum class eFileType {
OBJ,
MTL,
};
enum class eOBJSyntaxElement {
vertex_coords,
uv_vertex_coords,
normal,
poly_element_begin,
vertex_uv_normal_indices,
vertex_normal_indices,
vertex_uv_indices,
vertex_indices,
poly_element_end,
poly_usemtl,
edge,
cstype,
nurbs_degree,
curve_element_begin,
curve_element_end,
nurbs_parameter_begin,
nurbs_parameters,
nurbs_parameter_end,
nurbs_group_end,
new_line,
mtllib,
smooth_group,
object_group,
object_name,
/* Use rarely. New line is NOT included for string. */
string,
};
enum class eMTLSyntaxElement {
newmtl,
Ni,
d,
Ns,
illum,
Ka,
Kd,
Ks,
Ke,
map_Kd,
map_Ks,
map_Ns,
map_d,
map_refl,
map_Ke,
map_Bump,
/* Use rarely. New line is NOT included for string. */
string,
};
template<eFileType filetype> struct FileTypeTraits;
template<> struct FileTypeTraits<eFileType::OBJ> {
using SyntaxType = eOBJSyntaxElement;
};
template<> struct FileTypeTraits<eFileType::MTL> {
using SyntaxType = eMTLSyntaxElement;
};
template<eFileType type> struct Formatting {
const char *fmt = nullptr;
const int total_args = 0;
/* Fail to compile by default. */
const bool is_type_valid = false;
};
/**
* Type dependent but always false. Use to add a conditional compile-time error.
*/
template<typename T> struct always_false : std::false_type {
};
template<typename... T>
constexpr bool is_type_float = (... && std::is_floating_point_v<std::decay_t<T>>);
template<typename... T>
constexpr bool is_type_integral = (... && std::is_integral_v<std::decay_t<T>>);
template<typename... T>
constexpr bool is_type_string_related = (... && std::is_constructible_v<std::string, T>);
template<eFileType filetype, typename... T>
constexpr std::enable_if_t<filetype == eFileType::OBJ, Formatting<filetype>>
syntax_elem_to_formatting(const eOBJSyntaxElement key)
{
switch (key) {
case eOBJSyntaxElement::vertex_coords: {
return {"v %f %f %f\n", 3, is_type_float<T...>};
}
case eOBJSyntaxElement::uv_vertex_coords: {
return {"vt %f %f\n", 2, is_type_float<T...>};
}
case eOBJSyntaxElement::normal: {
return {"vn %f %f %f\n", 3, is_type_float<T...>};
}
case eOBJSyntaxElement::poly_element_begin: {
return {"f", 0, is_type_string_related<T...>};
}
case eOBJSyntaxElement::vertex_uv_normal_indices: {
return {" %d/%d/%d", 3, is_type_integral<T...>};
}
case eOBJSyntaxElement::vertex_normal_indices: {
return {" %d//%d", 2, is_type_integral<T...>};
}
case eOBJSyntaxElement::vertex_uv_indices: {
return {" %d/%d", 2, is_type_integral<T...>};
}
case eOBJSyntaxElement::vertex_indices: {
return {" %d", 1, is_type_integral<T...>};
}
case eOBJSyntaxElement::poly_usemtl: {
return {"usemtl %s\n", 1, is_type_string_related<T...>};
}
case eOBJSyntaxElement::edge: {
return {"l %d %d\n", 2, is_type_integral<T...>};
}
case eOBJSyntaxElement::cstype: {
return {"cstype bspline\n", 0, is_type_string_related<T...>};
}
case eOBJSyntaxElement::nurbs_degree: {
return {"deg %d\n", 1, is_type_integral<T...>};
}
case eOBJSyntaxElement::curve_element_begin: {
return {"curv 0.0 1.0", 0, is_type_string_related<T...>};
}
case eOBJSyntaxElement::nurbs_parameter_begin: {
return {"parm 0.0", 0, is_type_string_related<T...>};
}
case eOBJSyntaxElement::nurbs_parameters: {
return {" %f", 1, is_type_float<T...>};
}
case eOBJSyntaxElement::nurbs_parameter_end: {
return {" 1.0\n", 0, is_type_string_related<T...>};
}
case eOBJSyntaxElement::nurbs_group_end: {
return {"end\n", 0, is_type_string_related<T...>};
}
case eOBJSyntaxElement::poly_element_end: {
ATTR_FALLTHROUGH;
}
case eOBJSyntaxElement::curve_element_end: {
ATTR_FALLTHROUGH;
}
case eOBJSyntaxElement::new_line: {
return {"\n", 0, is_type_string_related<T...>};
}
case eOBJSyntaxElement::mtllib: {
return {"mtllib %s\n", 1, is_type_string_related<T...>};
}
case eOBJSyntaxElement::smooth_group: {
return {"s %d\n", 1, is_type_integral<T...>};
}
case eOBJSyntaxElement::object_group: {
return {"g %s\n", 1, is_type_string_related<T...>};
}
case eOBJSyntaxElement::object_name: {
return {"o %s\n", 1, is_type_string_related<T...>};
}
case eOBJSyntaxElement::string: {
return {"%s", 1, is_type_string_related<T...>};
}
}
}
template<eFileType filetype, typename... T>
constexpr std::enable_if_t<filetype == eFileType::MTL, Formatting<filetype>>
syntax_elem_to_formatting(const eMTLSyntaxElement key)
{
switch (key) {
case eMTLSyntaxElement::newmtl: {
return {"newmtl %s\n", 1, is_type_string_related<T...>};
}
case eMTLSyntaxElement::Ni: {
return {"Ni %.6f\n", 1, is_type_float<T...>};
}
case eMTLSyntaxElement::d: {
return {"d %.6f\n", 1, is_type_float<T...>};
}
case eMTLSyntaxElement::Ns: {
return {"Ns %.6f\n", 1, is_type_float<T...>};
}
case eMTLSyntaxElement::illum: {
return {"illum %d\n", 1, is_type_integral<T...>};
}
case eMTLSyntaxElement::Ka: {
return {"Ka %.6f %.6f %.6f\n", 3, is_type_float<T...>};
}
case eMTLSyntaxElement::Kd: {
return {"Kd %.6f %.6f %.6f\n", 3, is_type_float<T...>};
}
case eMTLSyntaxElement::Ks: {
return {"Ks %.6f %.6f %.6f\n", 3, is_type_float<T...>};
}
case eMTLSyntaxElement::Ke: {
return {"Ke %.6f %.6f %.6f\n", 3, is_type_float<T...>};
}
/* Keep only one space between options since filepaths may have leading spaces too. */
case eMTLSyntaxElement::map_Kd: {
return {"map_Kd %s %s\n", 2, is_type_string_related<T...>};
}
case eMTLSyntaxElement::map_Ks: {
return {"map_Ks %s %s\n", 2, is_type_string_related<T...>};
}
case eMTLSyntaxElement::map_Ns: {
return {"map_Ns %s %s\n", 2, is_type_string_related<T...>};
}
case eMTLSyntaxElement::map_d: {
return {"map_d %s %s\n", 2, is_type_string_related<T...>};
}
case eMTLSyntaxElement::map_refl: {
return {"map_refl %s %s\n", 2, is_type_string_related<T...>};
}
case eMTLSyntaxElement::map_Ke: {
return {"map_Ke %s %s\n", 2, is_type_string_related<T...>};
}
case eMTLSyntaxElement::map_Bump: {
return {"map_Bump %s %s\n", 2, is_type_string_related<T...>};
}
case eMTLSyntaxElement::string: {
return {"%s", 1, is_type_string_related<T...>};
}
}
}
template<eFileType filetype> class FileHandler : NonCopyable, NonMovable {
private:
FILE *outfile_ = nullptr;
std::string outfile_path_;
public:
FileHandler(std::string outfile_path) noexcept(false) : outfile_path_(std::move(outfile_path))
{
outfile_ = std::fopen(outfile_path_.c_str(), "w");
if (!outfile_) {
throw std::system_error(errno, std::system_category(), "Cannot open file");
}
}
~FileHandler()
{
if (outfile_ && std::fclose(outfile_)) {
std::cerr << "Error: could not close the file '" << outfile_path_
<< "' properly, it may be corrupted." << std::endl;
}
}
template<typename FileTypeTraits<filetype>::SyntaxType key, typename... T>
constexpr void write(T &&...args) const
{
constexpr Formatting<filetype> fmt_nargs_valid = syntax_elem_to_formatting<filetype, T...>(
key);
write__impl<fmt_nargs_valid.total_args>(fmt_nargs_valid.fmt, std::forward<T>(args)...);
/* Types of all arguments and the number of arguments should match
* what the formatting specifies. */
return std::enable_if_t < fmt_nargs_valid.is_type_valid &&
(sizeof...(T) == fmt_nargs_valid.total_args),
void > ();
}
private:
/* Remove this after upgrading to C++20. */
template<typename T> using remove_cvref_t = std::remove_cv_t<std::remove_reference_t<T>>;
/**
* Make #std::string etc., usable for fprintf-family.
* \return: `const char *` or the original argument if the argument is
* not related to #std::string.
*/
template<typename T> constexpr auto string_to_primitive(T &&arg) const
{
if constexpr (std::is_same_v<remove_cvref_t<T>, std::string> ||
std::is_same_v<remove_cvref_t<T>, blender::StringRefNull>) {
return arg.c_str();
}
else if constexpr (std::is_same_v<remove_cvref_t<T>, blender::StringRef>) {
BLI_STATIC_ASSERT(
(always_false<T>::value),
"Null-terminated string not present. Please use blender::StringRefNull instead.");
/* Another trick to cause a compile-time error: returning nothing to #std::printf. */
return;
}
else {
return std::forward<T>(arg);
}
}
template<int total_args, typename... T>
constexpr std::enable_if_t<(total_args != 0), void> write__impl(const char *fmt,
T &&...args) const
{
std::fprintf(outfile_, fmt, string_to_primitive(std::forward<T>(args))...);
}
template<int total_args, typename... T>
constexpr std::enable_if_t<(total_args == 0), void> write__impl(const char *fmt,
T &&...args) const
{
std::fputs(fmt, outfile_);
}
};
} // namespace blender::io::obj

View File

@ -0,0 +1,489 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup obj
*/
#include "BKE_customdata.h"
#include "BKE_deform.h"
#include "BKE_lib_id.h"
#include "BKE_material.h"
#include "BKE_mesh.h"
#include "BKE_mesh_mapping.h"
#include "BKE_object.h"
#include "BLI_listbase.h"
#include "BLI_math.h"
#include "DEG_depsgraph_query.h"
#include "DNA_material_types.h"
#include "DNA_mesh_types.h"
#include "DNA_modifier_types.h"
#include "DNA_object_types.h"
#include "obj_export_mesh.hh"
namespace blender::io::obj {
/**
* Store evaluated Object and Mesh pointers. Conditionally triangulate a mesh, or
* create a new Mesh from a Curve.
*/
OBJMesh::OBJMesh(Depsgraph *depsgraph, const OBJExportParams &export_params, Object *mesh_object)
{
export_object_eval_ = DEG_get_evaluated_object(depsgraph, mesh_object);
export_mesh_eval_ = BKE_object_get_evaluated_mesh(export_object_eval_);
mesh_eval_needs_free_ = false;
if (!export_mesh_eval_) {
/* Curves and NURBS surfaces need a new mesh when they're
* exported in the form of vertices and edges.
*/
export_mesh_eval_ = BKE_mesh_new_from_object(depsgraph, export_object_eval_, true, true);
/* Since a new mesh been allocated, it needs to be freed in the destructor. */
mesh_eval_needs_free_ = true;
}
if (export_params.export_triangulated_mesh &&
ELEM(export_object_eval_->type, OB_MESH, OB_SURF)) {
std::tie(export_mesh_eval_, mesh_eval_needs_free_) = triangulate_mesh_eval();
}
set_world_axes_transform(export_params.forward_axis, export_params.up_axis);
}
/**
* Free new meshes allocated for triangulated meshes, or Curve converted to Mesh.
*/
OBJMesh::~OBJMesh()
{
free_mesh_if_needed();
if (poly_smooth_groups_) {
MEM_freeN(poly_smooth_groups_);
}
}
/**
* Free the mesh if _the exporter_ created it.
*/
void OBJMesh::free_mesh_if_needed()
{
if (mesh_eval_needs_free_ && export_mesh_eval_) {
BKE_id_free(nullptr, export_mesh_eval_);
}
}
/**
* Allocate a new Mesh with triangulated polygons.
*
* The returned mesh can be the same as the old one.
* \return Owning pointer to the new Mesh, and whether a new Mesh was created.
*/
std::pair<Mesh *, bool> OBJMesh::triangulate_mesh_eval()
{
if (export_mesh_eval_->totpoly <= 0) {
return {export_mesh_eval_, false};
}
const struct BMeshCreateParams bm_create_params = {0u};
const struct BMeshFromMeshParams bm_convert_params = {1u, 0, 0, 0};
/* Lower threshold where triangulation of a polygon starts, i.e. a quadrilateral will be
* triangulated here. */
const int triangulate_min_verts = 4;
unique_bmesh_ptr bmesh(
BKE_mesh_to_bmesh_ex(export_mesh_eval_, &bm_create_params, &bm_convert_params));
BM_mesh_triangulate(bmesh.get(),
MOD_TRIANGULATE_NGON_BEAUTY,
MOD_TRIANGULATE_QUAD_SHORTEDGE,
triangulate_min_verts,
false,
nullptr,
nullptr,
nullptr);
Mesh *triangulated = BKE_mesh_from_bmesh_for_eval_nomain(
bmesh.get(), nullptr, export_mesh_eval_);
free_mesh_if_needed();
return {triangulated, true};
}
/**
* Set the final transform after applying axes settings and an Object's world transform.
*/
void OBJMesh::set_world_axes_transform(const eTransformAxisForward forward,
const eTransformAxisUp up)
{
float axes_transform[3][3];
unit_m3(axes_transform);
/* +Y-forward and +Z-up are the default Blender axis settings. */
mat3_from_axis_conversion(OBJ_AXIS_Y_FORWARD, OBJ_AXIS_Z_UP, forward, up, axes_transform);
/* mat3_from_axis_conversion returns a transposed matrix! */
transpose_m3(axes_transform);
mul_m4_m3m4(world_and_axes_transform_, axes_transform, export_object_eval_->obmat);
/* mul_m4_m3m4 does not transform last row of obmat, i.e. location data. */
mul_v3_m3v3(world_and_axes_transform_[3], axes_transform, export_object_eval_->obmat[3]);
world_and_axes_transform_[3][3] = export_object_eval_->obmat[3][3];
}
int OBJMesh::tot_vertices() const
{
return export_mesh_eval_->totvert;
}
int OBJMesh::tot_polygons() const
{
return export_mesh_eval_->totpoly;
}
int OBJMesh::tot_uv_vertices() const
{
return tot_uv_vertices_;
}
int OBJMesh::tot_edges() const
{
return export_mesh_eval_->totedge;
}
/**
* \return Total materials in the object.
*/
int16_t OBJMesh::tot_materials() const
{
return export_mesh_eval_->totcol;
}
/**
* \return Smooth group of the polygon at the given index.
*/
int OBJMesh::ith_smooth_group(const int poly_index) const
{
/* Calculate smooth groups first: #OBJMesh::calc_smooth_groups. */
BLI_assert(tot_smooth_groups_ != -NEGATIVE_INIT);
BLI_assert(poly_smooth_groups_);
return poly_smooth_groups_[poly_index];
}
void OBJMesh::ensure_mesh_normals() const
{
BKE_mesh_ensure_normals(export_mesh_eval_);
BKE_mesh_calc_normals_split(export_mesh_eval_);
}
void OBJMesh::ensure_mesh_edges() const
{
BKE_mesh_calc_edges(export_mesh_eval_, true, false);
BKE_mesh_calc_edges_loose(export_mesh_eval_);
}
/**
* Calculate smooth groups of a smooth-shaded object.
* \return A polygon aligned array of smooth group numbers.
*/
void OBJMesh::calc_smooth_groups(const bool use_bitflags)
{
poly_smooth_groups_ = BKE_mesh_calc_smoothgroups(export_mesh_eval_->medge,
export_mesh_eval_->totedge,
export_mesh_eval_->mpoly,
export_mesh_eval_->totpoly,
export_mesh_eval_->mloop,
export_mesh_eval_->totloop,
&tot_smooth_groups_,
use_bitflags);
}
/**
* Return mat_nr-th material of the object. The given index should be zero-based.
*/
const Material *OBJMesh::get_object_material(const int16_t mat_nr) const
{
/* "+ 1" as material getter needs one-based indices. */
const Material *r_mat = BKE_object_material_get(export_object_eval_, mat_nr + 1);
#ifdef DEBUG
if (!r_mat) {
std::cerr << "Material not found for mat_nr = " << mat_nr << std::endl;
}
#endif
return r_mat;
}
bool OBJMesh::is_ith_poly_smooth(const int poly_index) const
{
return export_mesh_eval_->mpoly[poly_index].flag & ME_SMOOTH;
}
/**
* Returns a zero-based index of a polygon's material indexing into
* the Object's material slots.
*/
int16_t OBJMesh::ith_poly_matnr(const int poly_index) const
{
BLI_assert(poly_index < export_mesh_eval_->totpoly);
const int16_t r_mat_nr = export_mesh_eval_->mpoly[poly_index].mat_nr;
return r_mat_nr >= 0 ? r_mat_nr : NOT_FOUND;
}
/**
* Get object name as it appears in the outliner.
*/
const char *OBJMesh::get_object_name() const
{
return export_object_eval_->id.name + 2;
}
/**
* Get Object's Mesh's name.
*/
const char *OBJMesh::get_object_mesh_name() const
{
return export_mesh_eval_->id.name + 2;
}
/**
* Get object's material (at the given index) name. The given index should be zero-based.
*/
const char *OBJMesh::get_object_material_name(const int16_t mat_nr) const
{
const Material *mat = get_object_material(mat_nr);
if (!mat) {
return nullptr;
}
return mat->id.name + 2;
}
/**
* Calculate coordinates of the vertex at the given index.
*/
float3 OBJMesh::calc_vertex_coords(const int vert_index, const float scaling_factor) const
{
float3 r_coords;
copy_v3_v3(r_coords, export_mesh_eval_->mvert[vert_index].co);
mul_v3_fl(r_coords, scaling_factor);
mul_m4_v3(world_and_axes_transform_, r_coords);
return r_coords;
}
/**
* Calculate vertex indices of all vertices of the polygon at the given index.
*/
Vector<int> OBJMesh::calc_poly_vertex_indices(const int poly_index) const
{
const MPoly &mpoly = export_mesh_eval_->mpoly[poly_index];
const MLoop *mloop = &export_mesh_eval_->mloop[mpoly.loopstart];
const int totloop = mpoly.totloop;
Vector<int> r_poly_vertex_indices(totloop);
for (int loop_index = 0; loop_index < totloop; loop_index++) {
r_poly_vertex_indices[loop_index] = mloop[loop_index].v;
}
return r_poly_vertex_indices;
}
/**
* Calculate UV vertex coordinates of an Object.
*
* \note Also store the UV vertex indices in the member variable.
*/
void OBJMesh::store_uv_coords_and_indices(Vector<std::array<float, 2>> &r_uv_coords)
{
const MPoly *mpoly = export_mesh_eval_->mpoly;
const MLoop *mloop = export_mesh_eval_->mloop;
const int totpoly = export_mesh_eval_->totpoly;
const int totvert = export_mesh_eval_->totvert;
const MLoopUV *mloopuv = static_cast<MLoopUV *>(
CustomData_get_layer(&export_mesh_eval_->ldata, CD_MLOOPUV));
if (!mloopuv) {
tot_uv_vertices_ = 0;
return;
}
const float limit[2] = {STD_UV_CONNECT_LIMIT, STD_UV_CONNECT_LIMIT};
UvVertMap *uv_vert_map = BKE_mesh_uv_vert_map_create(
mpoly, mloop, mloopuv, totpoly, totvert, limit, false, false);
uv_indices_.resize(totpoly);
/* At least total vertices of a mesh will be present in its texture map. So
* reserve minimum space early. */
r_uv_coords.reserve(totvert);
tot_uv_vertices_ = 0;
for (int vertex_index = 0; vertex_index < totvert; vertex_index++) {
const UvMapVert *uv_vert = BKE_mesh_uv_vert_map_get_vert(uv_vert_map, vertex_index);
for (; uv_vert; uv_vert = uv_vert->next) {
if (uv_vert->separate) {
tot_uv_vertices_ += 1;
}
const int vertices_in_poly = mpoly[uv_vert->poly_index].totloop;
/* Store UV vertex coordinates. */
r_uv_coords.resize(tot_uv_vertices_);
const int loopstart = mpoly[uv_vert->poly_index].loopstart;
Span<float> vert_uv_coords(mloopuv[loopstart + uv_vert->loop_of_poly_index].uv, 2);
r_uv_coords[tot_uv_vertices_ - 1][0] = vert_uv_coords[0];
r_uv_coords[tot_uv_vertices_ - 1][1] = vert_uv_coords[1];
/* Store UV vertex indices. */
uv_indices_[uv_vert->poly_index].resize(vertices_in_poly);
/* Keep indices zero-based and let the writer handle the "+ 1" as per OBJ spec. */
uv_indices_[uv_vert->poly_index][uv_vert->loop_of_poly_index] = tot_uv_vertices_ - 1;
}
}
BKE_mesh_uv_vert_map_free(uv_vert_map);
}
Span<int> OBJMesh::calc_poly_uv_indices(const int poly_index) const
{
if (uv_indices_.size() <= 0) {
return {};
}
BLI_assert(poly_index < export_mesh_eval_->totpoly);
BLI_assert(poly_index < uv_indices_.size());
return uv_indices_[poly_index];
}
/**
* Calculate polygon normal of a polygon at given index.
*
* Should be used for flat-shaded polygons.
*/
float3 OBJMesh::calc_poly_normal(const int poly_index) const
{
float3 r_poly_normal;
const MPoly &poly = export_mesh_eval_->mpoly[poly_index];
const MLoop &mloop = export_mesh_eval_->mloop[poly.loopstart];
const MVert &mvert = *(export_mesh_eval_->mvert);
BKE_mesh_calc_poly_normal(&poly, &mloop, &mvert, r_poly_normal);
mul_mat3_m4_v3(world_and_axes_transform_, r_poly_normal);
return r_poly_normal;
}
/**
* Calculate loop normals of a polygon at the given index.
*
* Should be used for smooth-shaded polygons.
*/
void OBJMesh::calc_loop_normals(const int poly_index, Vector<float3> &r_loop_normals) const
{
r_loop_normals.clear();
const MPoly &mpoly = export_mesh_eval_->mpoly[poly_index];
const float(
*lnors)[3] = (const float(*)[3])(CustomData_get_layer(&export_mesh_eval_->ldata, CD_NORMAL));
for (int loop_of_poly = 0; loop_of_poly < mpoly.totloop; loop_of_poly++) {
float3 loop_normal;
copy_v3_v3(loop_normal, lnors[mpoly.loopstart + loop_of_poly]);
mul_mat3_m4_v3(world_and_axes_transform_, loop_normal);
r_loop_normals.append(loop_normal);
}
}
/**
* Calculate a polygon's polygon/loop normal indices.
* \param object_tot_prev_normals Number of normals of this Object written so far.
* \return Number of distinct normal indices.
*/
std::pair<int, Vector<int>> OBJMesh::calc_poly_normal_indices(
const int poly_index, const int object_tot_prev_normals) const
{
const MPoly &mpoly = export_mesh_eval_->mpoly[poly_index];
const int totloop = mpoly.totloop;
Vector<int> r_poly_normal_indices(totloop);
if (is_ith_poly_smooth(poly_index)) {
for (int poly_loop_index = 0; poly_loop_index < totloop; poly_loop_index++) {
/* Using polygon loop index is fine because polygon/loop normals and their normal indices are
* written by looping over #Mesh.mpoly /#Mesh.mloop in the same order. */
r_poly_normal_indices[poly_loop_index] = object_tot_prev_normals + poly_loop_index;
}
/* For a smooth-shaded polygon, #Mesh.totloop -many loop normals are written. */
return {totloop, r_poly_normal_indices};
}
for (int poly_loop_index = 0; poly_loop_index < totloop; poly_loop_index++) {
r_poly_normal_indices[poly_loop_index] = object_tot_prev_normals;
}
/* For a flat-shaded polygon, one polygon normal is written. */
return {1, r_poly_normal_indices};
}
/**
* Find the index of the vertex group with the maximum number of vertices in a polygon.
* The index indices into the #Object.defbase.
*
* If two or more groups have the same number of vertices (maximum), group name depends on the
* implementation of #std::max_element.
*/
int16_t OBJMesh::get_poly_deform_group_index(const int poly_index) const
{
BLI_assert(poly_index < export_mesh_eval_->totpoly);
const MPoly &mpoly = export_mesh_eval_->mpoly[poly_index];
const MLoop *mloop = &export_mesh_eval_->mloop[mpoly.loopstart];
const Object *obj = export_object_eval_;
const int tot_deform_groups = BKE_object_defgroup_count(obj);
/* Indices of the vector index into deform groups of an object; values are the]
* number of vertex members in one deform group. */
Vector<int16_t> deform_group_members(tot_deform_groups, 0);
/* Whether at least one vertex in the polygon belongs to any group. */
bool found_group = false;
const MDeformVert *dvert_orig = static_cast<MDeformVert *>(
CustomData_get_layer(&export_mesh_eval_->vdata, CD_MDEFORMVERT));
if (!dvert_orig) {
return NOT_FOUND;
}
const MDeformWeight *curr_weight = nullptr;
const MDeformVert *dvert = nullptr;
for (int loop_index = 0; loop_index < mpoly.totloop; loop_index++) {
dvert = &dvert_orig[(mloop + loop_index)->v];
curr_weight = dvert->dw;
if (curr_weight) {
bDeformGroup *vertex_group = static_cast<bDeformGroup *>(
BLI_findlink(BKE_object_defgroup_list(obj), curr_weight->def_nr));
if (vertex_group) {
deform_group_members[curr_weight->def_nr] += 1;
found_group = true;
}
}
}
if (!found_group) {
return NOT_FOUND;
}
/* Index of the group with maximum vertices. */
int16_t max_idx = std::max_element(deform_group_members.begin(), deform_group_members.end()) -
deform_group_members.begin();
return max_idx;
}
/**
* Find the name of the vertex deform group at the given index.
* The index indices into the #Object.defbase.
*/
const char *OBJMesh::get_poly_deform_group_name(const int16_t def_group_index) const
{
const bDeformGroup &vertex_group = *(static_cast<bDeformGroup *>(
BLI_findlink(BKE_object_defgroup_list(export_object_eval_), def_group_index)));
return vertex_group.name;
}
/**
* Calculate vertex indices of an edge's corners if it is a loose edge.
*/
std::optional<std::array<int, 2>> OBJMesh::calc_loose_edge_vert_indices(const int edge_index) const
{
const MEdge &edge = export_mesh_eval_->medge[edge_index];
if (edge.flag & ME_LOOSEEDGE) {
return std::array<int, 2>{static_cast<int>(edge.v1), static_cast<int>(edge.v2)};
}
return std::nullopt;
}
} // namespace blender::io::obj

View File

@ -0,0 +1,131 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup obj
*/
#pragma once
#include <optional>
#include "BLI_float3.hh"
#include "BLI_utility_mixins.hh"
#include "BLI_vector.hh"
#include "bmesh.h"
#include "bmesh_tools.h"
#include "DNA_material_types.h"
#include "DNA_mesh_types.h"
#include "DNA_meshdata_types.h"
#include "IO_wavefront_obj.h"
namespace blender::io::obj {
/* Denote absence for usually non-negative numbers. */
const int NOT_FOUND = -1;
/* Any negative number other than `NOT_FOUND` to initialise usually non-negative numbers. */
const int NEGATIVE_INIT = -10;
/**
* #std::unique_ptr deleter for BMesh.
*/
struct CustomBMeshDeleter {
void operator()(BMesh *bmesh)
{
if (bmesh) {
BM_mesh_free(bmesh);
}
}
};
using unique_bmesh_ptr = std::unique_ptr<BMesh, CustomBMeshDeleter>;
class OBJMesh : NonCopyable {
private:
Object *export_object_eval_;
Mesh *export_mesh_eval_;
/**
* For curves which are converted to mesh, and triangulated meshes, a new mesh is allocated.
*/
bool mesh_eval_needs_free_ = false;
/**
* Final transform of an object obtained from export settings (up_axis, forward_axis) and the
* object's world transform matrix.
*/
float world_and_axes_transform_[4][4];
/**
* Total UV vertices in a mesh's texture map.
*/
int tot_uv_vertices_ = 0;
/**
* Per-polygon-per-vertex UV vertex indices.
*/
Vector<Vector<int>> uv_indices_;
/**
* Total smooth groups in an object.
*/
int tot_smooth_groups_ = NEGATIVE_INIT;
/**
* Polygon aligned array of their smooth groups.
*/
int *poly_smooth_groups_ = nullptr;
public:
OBJMesh(Depsgraph *depsgraph, const OBJExportParams &export_params, Object *mesh_object);
~OBJMesh();
int tot_vertices() const;
int tot_polygons() const;
int tot_uv_vertices() const;
int tot_edges() const;
int16_t tot_materials() const;
const Material *get_object_material(const int16_t mat_nr) const;
int16_t ith_poly_matnr(const int poly_index) const;
void ensure_mesh_normals() const;
void ensure_mesh_edges() const;
void calc_smooth_groups(const bool use_bitflags);
int ith_smooth_group(const int poly_index) const;
bool is_ith_poly_smooth(const int poly_index) const;
const char *get_object_name() const;
const char *get_object_mesh_name() const;
const char *get_object_material_name(const int16_t mat_nr) const;
float3 calc_vertex_coords(const int vert_index, const float scaling_factor) const;
Vector<int> calc_poly_vertex_indices(const int poly_index) const;
void store_uv_coords_and_indices(Vector<std::array<float, 2>> &r_uv_coords);
Span<int> calc_poly_uv_indices(const int poly_index) const;
float3 calc_poly_normal(const int poly_index) const;
std::pair<int, Vector<int>> calc_poly_normal_indices(const int poly_index,
const int object_tot_prev_normals) const;
void calc_loop_normals(const int poly_index, Vector<float3> &r_loop_normals) const;
int16_t get_poly_deform_group_index(const int poly_index) const;
const char *get_poly_deform_group_name(const int16_t def_group_index) const;
std::optional<std::array<int, 2>> calc_loose_edge_vert_indices(const int edge_index) const;
private:
void free_mesh_if_needed();
std::pair<Mesh *, bool> triangulate_mesh_eval();
void set_world_axes_transform(const eTransformAxisForward forward, const eTransformAxisUp up);
};
} // namespace blender::io::obj

View File

@ -0,0 +1,362 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup obj
*/
#include "BKE_image.h"
#include "BKE_node.h"
#include "BLI_float3.hh"
#include "BLI_map.hh"
#include "BLI_path_util.h"
#include "DNA_material_types.h"
#include "DNA_node_types.h"
#include "NOD_node_tree_ref.hh"
#include "obj_export_mesh.hh"
#include "obj_export_mtl.hh"
namespace blender::io::obj {
/**
* Copy a float property of the given type from the bNode to given buffer.
*/
static void copy_property_from_node(const eNodeSocketDatatype property_type,
const bNode *node,
const char *identifier,
MutableSpan<float> r_property)
{
if (!node) {
return;
}
bNodeSocket *socket{nodeFindSocket(node, SOCK_IN, identifier)};
BLI_assert(socket && socket->type == property_type);
if (!socket) {
return;
}
switch (property_type) {
case SOCK_FLOAT: {
BLI_assert(r_property.size() == 1);
bNodeSocketValueFloat *socket_def_value = static_cast<bNodeSocketValueFloat *>(
socket->default_value);
r_property[0] = socket_def_value->value;
break;
}
case SOCK_RGBA: {
BLI_assert(r_property.size() == 3);
bNodeSocketValueRGBA *socket_def_value = static_cast<bNodeSocketValueRGBA *>(
socket->default_value);
copy_v3_v3(r_property.data(), socket_def_value->value);
break;
}
case SOCK_VECTOR: {
BLI_assert(r_property.size() == 3);
bNodeSocketValueVector *socket_def_value = static_cast<bNodeSocketValueVector *>(
socket->default_value);
copy_v3_v3(r_property.data(), socket_def_value->value);
break;
}
default: {
/* Other socket types are not handled here. */
BLI_assert(0);
break;
}
}
}
/**
* Collect all the source sockets linked to the destination socket in a destination node.
*/
static void linked_sockets_to_dest_id(const bNode *dest_node,
const nodes::NodeTreeRef &node_tree,
StringRefNull dest_socket_id,
Vector<const nodes::OutputSocketRef *> &r_linked_sockets)
{
r_linked_sockets.clear();
if (!dest_node) {
return;
}
Span<const nodes::NodeRef *> object_dest_nodes = node_tree.nodes_by_type(dest_node->idname);
Span<const nodes::InputSocketRef *> dest_inputs = object_dest_nodes.first()->inputs();
const nodes::InputSocketRef *dest_socket = nullptr;
for (const nodes::InputSocketRef *curr_socket : dest_inputs) {
if (STREQ(curr_socket->bsocket()->identifier, dest_socket_id.c_str())) {
dest_socket = curr_socket;
break;
}
}
if (dest_socket) {
Span<const nodes::OutputSocketRef *> linked_sockets = dest_socket->directly_linked_sockets();
r_linked_sockets.resize(linked_sockets.size());
r_linked_sockets = linked_sockets;
}
}
/**
* From a list of sockets, get the parent node which is of the given node type.
*/
static const bNode *get_node_of_type(Span<const nodes::OutputSocketRef *> sockets_list,
const int node_type)
{
for (const nodes::SocketRef *socket : sockets_list) {
const bNode *parent_node = socket->bnode();
if (parent_node->typeinfo->type == node_type) {
return parent_node;
}
}
return nullptr;
}
/**
* From a texture image shader node, get the image's filepath.
* Returned filepath is stripped of initial "//". If packed image is found,
* only the file "name" is returned.
*/
static const char *get_image_filepath(const bNode *tex_node)
{
if (!tex_node) {
return nullptr;
}
Image *tex_image = reinterpret_cast<Image *>(tex_node->id);
if (!tex_image || !BKE_image_has_filepath(tex_image)) {
return nullptr;
}
const char *path = tex_image->filepath;
if (BKE_image_has_packedfile(tex_image)) {
/* Put image in the same directory as the .MTL file. */
path = BLI_path_slash_rfind(path) + 1;
fprintf(stderr,
"Packed image found:'%s'. Unpack and place the image in the same "
"directory as the .MTL file.\n",
path);
}
if (path[0] == '/' && path[1] == '/') {
path += 2;
}
return path;
}
/**
* Find the Principled-BSDF Node in nodetree.
* We only want one that feeds directly into a Material Output node
* (that is the behavior of the legacy Python exporter).
*/
static const nodes::NodeRef *find_bsdf_node(const nodes::NodeTreeRef *nodetree)
{
if (!nodetree) {
return nullptr;
}
for (const nodes::NodeRef *node : nodetree->nodes_by_type("ShaderNodeOutputMaterial")) {
const nodes::InputSocketRef *node_input_socket0 = node->inputs()[0];
for (const nodes::OutputSocketRef *out_sock : node_input_socket0->directly_linked_sockets()) {
const nodes::NodeRef &in_node = out_sock->node();
if (in_node.typeinfo()->type == SH_NODE_BSDF_PRINCIPLED) {
return &in_node;
}
}
}
return nullptr;
}
/**
* Store properties found either in bNode or material into r_mtl_mat.
*/
static void store_bsdf_properties(const nodes::NodeRef *bsdf_node,
const Material *material,
MTLMaterial &r_mtl_mat)
{
const bNode *bnode = nullptr;
if (bsdf_node) {
bnode = bsdf_node->bnode();
}
/* If p-BSDF is not present, fallback to #Object.Material. */
float roughness = material->roughness;
if (bnode) {
copy_property_from_node(SOCK_FLOAT, bnode, "Roughness", {&roughness, 1});
}
/* Emperical approximation. Importer should use the inverse of this method. */
float spec_exponent = (1.0f - roughness) * 30;
spec_exponent *= spec_exponent;
float specular = material->spec;
if (bnode) {
copy_property_from_node(SOCK_FLOAT, bnode, "Specular", {&specular, 1});
}
float metallic = material->metallic;
if (bnode) {
copy_property_from_node(SOCK_FLOAT, bnode, "Metallic", {&metallic, 1});
}
float refraction_index = 1.0f;
if (bnode) {
copy_property_from_node(SOCK_FLOAT, bnode, "IOR", {&refraction_index, 1});
}
float dissolved = material->a;
if (bnode) {
copy_property_from_node(SOCK_FLOAT, bnode, "Alpha", {&dissolved, 1});
}
const bool transparent = dissolved != 1.0f;
float3 diffuse_col = {material->r, material->g, material->b};
if (bnode) {
copy_property_from_node(SOCK_RGBA, bnode, "Base Color", {diffuse_col, 3});
}
float3 emission_col{0.0f};
float emission_strength = 0.0f;
if (bnode) {
copy_property_from_node(SOCK_FLOAT, bnode, "Emission Strength", {&emission_strength, 1});
copy_property_from_node(SOCK_RGBA, bnode, "Emission", {emission_col, 3});
}
mul_v3_fl(emission_col, emission_strength);
/* See https://wikipedia.org/wiki/Wavefront_.obj_file for all possible values of illum. */
/* Highlight on. */
int illum = 2;
if (specular == 0.0f) {
/* Color on and Ambient on. */
illum = 1;
}
else if (metallic > 0.0f) {
/* Metallic ~= Reflection. */
if (transparent) {
/* Transparency: Refraction on, Reflection: ~~Fresnel off and Ray trace~~ on. */
illum = 6;
}
else {
/* Reflection on and Ray trace on. */
illum = 3;
}
}
else if (transparent) {
/* Transparency: Glass on, Reflection: Ray trace off */
illum = 9;
}
r_mtl_mat.Ns = spec_exponent;
if (metallic != 0.0f) {
r_mtl_mat.Ka = {metallic, metallic, metallic};
}
else {
r_mtl_mat.Ka = {1.0f, 1.0f, 1.0f};
}
r_mtl_mat.Kd = diffuse_col;
r_mtl_mat.Ks = {specular, specular, specular};
r_mtl_mat.Ke = emission_col;
r_mtl_mat.Ni = refraction_index;
r_mtl_mat.d = dissolved;
r_mtl_mat.illum = illum;
}
/**
* Store image texture options and filepaths in r_mtl_mat.
*/
static void store_image_textures(const nodes::NodeRef *bsdf_node,
const nodes::NodeTreeRef *node_tree,
const Material *material,
MTLMaterial &r_mtl_mat)
{
if (!material || !node_tree || !bsdf_node) {
/* No nodetree, no images, or no Principled BSDF node. */
return;
}
const bNode *bnode = bsdf_node->bnode();
/* Normal Map Texture has two extra tasks of:
* - finding a Normal Map node before finding a texture node.
* - finding "Strength" property of the node for `-bm` option.
*/
for (Map<const eMTLSyntaxElement, tex_map_XX>::MutableItem texture_map :
r_mtl_mat.texture_maps.items()) {
Vector<const nodes::OutputSocketRef *> linked_sockets;
const bNode *normal_map_node{nullptr};
if (texture_map.key == eMTLSyntaxElement::map_Bump) {
/* Find sockets linked to destination "Normal" socket in p-bsdf node. */
linked_sockets_to_dest_id(bnode, *node_tree, "Normal", linked_sockets);
/* Among the linked sockets, find Normal Map shader node. */
normal_map_node = get_node_of_type(linked_sockets, SH_NODE_NORMAL_MAP);
/* Find sockets linked to "Color" socket in normal map node. */
linked_sockets_to_dest_id(normal_map_node, *node_tree, "Color", linked_sockets);
}
else if (texture_map.key == eMTLSyntaxElement::map_Ke) {
float emission_strength = 0.0f;
copy_property_from_node(SOCK_FLOAT, bnode, "Emission Strength", {&emission_strength, 1});
if (emission_strength == 0.0f) {
continue;
}
}
else {
/* Find sockets linked to the destination socket of interest, in p-bsdf node. */
linked_sockets_to_dest_id(
bnode, *node_tree, texture_map.value.dest_socket_id, linked_sockets);
}
/* Among the linked sockets, find Image Texture shader node. */
const bNode *tex_node{get_node_of_type(linked_sockets, SH_NODE_TEX_IMAGE)};
if (!tex_node) {
continue;
}
const char *tex_image_filepath = get_image_filepath(tex_node);
if (!tex_image_filepath) {
continue;
}
/* Find "Mapping" node if connected to texture node. */
linked_sockets_to_dest_id(tex_node, *node_tree, "Vector", linked_sockets);
const bNode *mapping = get_node_of_type(linked_sockets, SH_NODE_MAPPING);
if (normal_map_node) {
copy_property_from_node(
SOCK_FLOAT, normal_map_node, "Strength", {&r_mtl_mat.map_Bump_strength, 1});
}
/* Texture transform options. Only translation (origin offset, "-o") and scale
* ("-o") are supported. */
copy_property_from_node(SOCK_VECTOR, mapping, "Location", {texture_map.value.translation, 3});
copy_property_from_node(SOCK_VECTOR, mapping, "Scale", {texture_map.value.scale, 3});
texture_map.value.image_path = tex_image_filepath;
}
}
MTLMaterial mtlmaterial_for_material(const Material *material)
{
BLI_assert(material != nullptr);
MTLMaterial mtlmat;
mtlmat.name = std::string(material->id.name + 2);
std::replace(mtlmat.name.begin(), mtlmat.name.end(), ' ', '_');
const nodes::NodeTreeRef *nodetree = nullptr;
if (material->nodetree) {
nodetree = new nodes::NodeTreeRef(material->nodetree);
}
const nodes::NodeRef *bsdf_node = find_bsdf_node(nodetree);
store_bsdf_properties(bsdf_node, material, mtlmat);
store_image_textures(bsdf_node, nodetree, material, mtlmat);
if (nodetree) {
delete nodetree;
}
return mtlmat;
}
} // namespace blender::io::obj

View File

@ -0,0 +1,104 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup obj
*/
#pragma once
#include "BLI_float3.hh"
#include "BLI_map.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "DNA_node_types.h"
#include "obj_export_io.hh"
namespace blender {
template<> struct DefaultHash<io::obj::eMTLSyntaxElement> {
uint64_t operator()(const io::obj::eMTLSyntaxElement value) const
{
return static_cast<uint64_t>(value);
}
};
} // namespace blender
namespace blender::io::obj {
class OBJMesh;
/**
* Generic container for texture node properties.
*/
struct tex_map_XX {
tex_map_XX(StringRef to_socket_id) : dest_socket_id(to_socket_id){};
/** Target socket which this texture node connects to. */
const std::string dest_socket_id;
float3 translation{0.0f};
float3 scale{1.0f};
/* Only Flat and Smooth projections are supported. */
int projection_type = SHD_PROJ_FLAT;
std::string image_path;
std::string mtl_dir_path;
};
/**
* Container suited for storing Material data for/from a .MTL file.
*/
struct MTLMaterial {
MTLMaterial()
{
texture_maps.add(eMTLSyntaxElement::map_Kd, tex_map_XX("Base Color"));
texture_maps.add(eMTLSyntaxElement::map_Ks, tex_map_XX("Specular"));
texture_maps.add(eMTLSyntaxElement::map_Ns, tex_map_XX("Roughness"));
texture_maps.add(eMTLSyntaxElement::map_d, tex_map_XX("Alpha"));
texture_maps.add(eMTLSyntaxElement::map_refl, tex_map_XX("Metallic"));
texture_maps.add(eMTLSyntaxElement::map_Ke, tex_map_XX("Emission"));
texture_maps.add(eMTLSyntaxElement::map_Bump, tex_map_XX("Normal"));
}
/**
* Caller must ensure that the given lookup key exists in the Map.
* \return Texture map corresponding to the given ID.
*/
tex_map_XX &tex_map_of_type(const eMTLSyntaxElement key)
{
{
BLI_assert(texture_maps.contains_as(key));
return texture_maps.lookup_as(key);
}
}
std::string name;
/* Always check for negative values while importing or exporting. Use defaults if
* any value is negative. */
float Ns{-1.0f};
float3 Ka{-1.0f};
float3 Kd{-1.0f};
float3 Ks{-1.0f};
float3 Ke{-1.0f};
float Ni{-1.0f};
float d{-1.0f};
int illum{-1};
Map<const eMTLSyntaxElement, tex_map_XX> texture_maps;
/** Only used for Normal Map node: "map_Bump". */
float map_Bump_strength{-1.0f};
};
MTLMaterial mtlmaterial_for_material(const Material *material);
} // namespace blender::io::obj

View File

@ -0,0 +1,122 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup obj
*/
#include "BLI_float3.hh"
#include "BLI_listbase.h"
#include "BLI_math.h"
#include "DEG_depsgraph.h"
#include "DEG_depsgraph_query.h"
#include "IO_wavefront_obj.h"
#include "obj_export_nurbs.hh"
namespace blender::io::obj {
OBJCurve::OBJCurve(const Depsgraph *depsgraph,
const OBJExportParams &export_params,
Object *curve_object)
: export_object_eval_(curve_object)
{
export_object_eval_ = DEG_get_evaluated_object(depsgraph, curve_object);
export_curve_ = static_cast<Curve *>(export_object_eval_->data);
set_world_axes_transform(export_params.forward_axis, export_params.up_axis);
}
/**
* Set the final transform after applying axes settings and an Object's world transform.
*/
void OBJCurve::set_world_axes_transform(const eTransformAxisForward forward,
const eTransformAxisUp up)
{
float axes_transform[3][3];
unit_m3(axes_transform);
/* +Y-forward and +Z-up are the Blender's default axis settings. */
mat3_from_axis_conversion(OBJ_AXIS_Y_FORWARD, OBJ_AXIS_Z_UP, forward, up, axes_transform);
/* mat3_from_axis_conversion returns a transposed matrix! */
transpose_m3(axes_transform);
mul_m4_m3m4(world_axes_transform_, axes_transform, export_object_eval_->obmat);
/* #mul_m4_m3m4 does not transform last row of #Object.obmat, i.e. location data. */
mul_v3_m3v3(world_axes_transform_[3], axes_transform, export_object_eval_->obmat[3]);
world_axes_transform_[3][3] = export_object_eval_->obmat[3][3];
}
const char *OBJCurve::get_curve_name() const
{
return export_object_eval_->id.name + 2;
}
int OBJCurve::total_splines() const
{
return BLI_listbase_count(&export_curve_->nurb);
}
/**
* \param spline_index: Zero-based index of spline of interest.
* \return: Total vertices in a spline.
*/
int OBJCurve::total_spline_vertices(const int spline_index) const
{
const Nurb *const nurb = static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index));
return nurb->pntsu * nurb->pntsv;
}
/**
* Get coordinates of the vertex at the given index on the given spline.
*/
float3 OBJCurve::vertex_coordinates(const int spline_index,
const int vertex_index,
const float scaling_factor) const
{
const Nurb *const nurb = static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index));
float3 r_coord;
const BPoint &bpoint = nurb->bp[vertex_index];
copy_v3_v3(r_coord, bpoint.vec);
mul_m4_v3(world_axes_transform_, r_coord);
mul_v3_fl(r_coord, scaling_factor);
return r_coord;
}
/**
* Get total control points of the NURBS spline at the given index. This is different than total
* vertices of a spline.
*/
int OBJCurve::total_spline_control_points(const int spline_index) const
{
const Nurb *const nurb = static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index));
const int r_nurbs_degree = nurb->orderu - 1;
/* Total control points = Number of points in the curve (+ degree of the
* curve if it is cyclic). */
int r_tot_control_points = nurb->pntsv * nurb->pntsu;
if (nurb->flagu & CU_NURB_CYCLIC) {
r_tot_control_points += r_nurbs_degree;
}
return r_tot_control_points;
}
/**
* Get the degree of the NURBS spline at the given index.
*/
int OBJCurve::get_nurbs_degree(const int spline_index) const
{
const Nurb *const nurb = static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index));
return nurb->orderu - 1;
}
} // namespace blender::io::obj

View File

@ -0,0 +1,57 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup obj
*/
#pragma once
#include "BLI_utility_mixins.hh"
#include "DNA_curve_types.h"
namespace blender::io::obj {
/**
* Provides access to the a Curve Object's properties.
* Only #CU_NURBS type is supported.
*
* \note Used for Curves to be exported in parameter form, and not converted to meshes.
*/
class OBJCurve : NonCopyable {
private:
const Object *export_object_eval_;
const Curve *export_curve_;
float world_axes_transform_[4][4];
public:
OBJCurve(const Depsgraph *depsgraph, const OBJExportParams &export_params, Object *curve_object);
const char *get_curve_name() const;
int total_splines() const;
int total_spline_vertices(const int spline_index) const;
float3 vertex_coordinates(const int spline_index,
const int vertex_index,
const float scaling_factor) const;
int total_spline_control_points(const int spline_index) const;
int get_nurbs_degree(const int spline_index) const;
private:
void set_world_axes_transform(const eTransformAxisForward forward, const eTransformAxisUp up);
};
} // namespace blender::io::obj

View File

@ -0,0 +1,302 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup obj
*/
#include <cstdio>
#include <exception>
#include <memory>
#include "BKE_scene.h"
#include "BLI_path_util.h"
#include "BLI_vector.hh"
#include "DEG_depsgraph_query.h"
#include "DNA_scene_types.h"
#include "ED_object.h"
#include "obj_export_mesh.hh"
#include "obj_export_nurbs.hh"
#include "obj_exporter.hh"
#include "obj_export_file_writer.hh"
namespace blender::io::obj {
OBJDepsgraph::OBJDepsgraph(const bContext *C, const eEvaluationMode eval_mode)
{
Scene *scene = CTX_data_scene(C);
Main *bmain = CTX_data_main(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
if (eval_mode == DAG_EVAL_RENDER) {
depsgraph_ = DEG_graph_new(bmain, scene, view_layer, DAG_EVAL_RENDER);
needs_free_ = true;
DEG_graph_build_for_all_objects(depsgraph_);
BKE_scene_graph_evaluated_ensure(depsgraph_, bmain);
}
else {
depsgraph_ = CTX_data_ensure_evaluated_depsgraph(C);
needs_free_ = false;
}
}
OBJDepsgraph::~OBJDepsgraph()
{
if (needs_free_) {
DEG_graph_free(depsgraph_);
}
}
Depsgraph *OBJDepsgraph::get()
{
return depsgraph_;
}
void OBJDepsgraph::update_for_newframe()
{
BKE_scene_graph_update_for_newframe(depsgraph_);
}
static void print_exception_error(const std::system_error &ex)
{
std::cerr << ex.code().category().name() << ": " << ex.what() << ": " << ex.code().message()
<< std::endl;
}
/**
* Filter supported objects from the Scene.
*
* \note Curves are also stored with Meshes if export settings specify so.
*/
std::pair<Vector<std::unique_ptr<OBJMesh>>, Vector<std::unique_ptr<OBJCurve>>>
filter_supported_objects(Depsgraph *depsgraph, const OBJExportParams &export_params)
{
Vector<std::unique_ptr<OBJMesh>> r_exportable_meshes;
Vector<std::unique_ptr<OBJCurve>> r_exportable_nurbs;
const ViewLayer *view_layer = DEG_get_input_view_layer(depsgraph);
LISTBASE_FOREACH (const Base *, base, &view_layer->object_bases) {
Object *object_in_layer = base->object;
if (export_params.export_selected_objects && !(object_in_layer->base_flag & BASE_SELECTED)) {
continue;
}
switch (object_in_layer->type) {
case OB_SURF:
/* Export in mesh form: vertices and polygons. */
ATTR_FALLTHROUGH;
case OB_MESH:
r_exportable_meshes.append(
std::make_unique<OBJMesh>(depsgraph, export_params, object_in_layer));
break;
case OB_CURVE: {
Curve *curve = static_cast<Curve *>(object_in_layer->data);
Nurb *nurb{static_cast<Nurb *>(curve->nurb.first)};
if (!nurb) {
/* An empty curve. Not yet supported to export these as meshes. */
if (export_params.export_curves_as_nurbs) {
r_exportable_nurbs.append(
std::make_unique<OBJCurve>(depsgraph, export_params, object_in_layer));
}
break;
}
switch (nurb->type) {
case CU_NURBS:
if (export_params.export_curves_as_nurbs) {
/* Export in parameter form: control points. */
r_exportable_nurbs.append(
std::make_unique<OBJCurve>(depsgraph, export_params, object_in_layer));
}
else {
/* Export in mesh form: edges and vertices. */
r_exportable_meshes.append(
std::make_unique<OBJMesh>(depsgraph, export_params, object_in_layer));
}
break;
case CU_BEZIER:
/* Always export in mesh form: edges and vertices. */
r_exportable_meshes.append(
std::make_unique<OBJMesh>(depsgraph, export_params, object_in_layer));
break;
default:
/* Other curve types are not supported. */
break;
}
break;
}
default:
/* Other object types are not supported. */
break;
}
}
return {std::move(r_exportable_meshes), std::move(r_exportable_nurbs)};
}
static void write_mesh_objects(Vector<std::unique_ptr<OBJMesh>> exportable_as_mesh,
OBJWriter &obj_writer,
MTLWriter *mtl_writer,
const OBJExportParams &export_params)
{
if (mtl_writer) {
obj_writer.write_mtllib_name(mtl_writer->mtl_file_path());
}
/* Smooth groups and UV vertex indices may make huge memory allocations, so they should be freed
* right after they're written, instead of waiting for #blender::Vector to clean them up after
* all the objects are exported. */
for (auto &obj_mesh : exportable_as_mesh) {
obj_writer.write_object_name(*obj_mesh);
obj_writer.write_vertex_coords(*obj_mesh);
Vector<int> obj_mtlindices;
if (obj_mesh->tot_polygons() > 0) {
if (export_params.export_smooth_groups) {
obj_mesh->calc_smooth_groups(export_params.smooth_groups_bitflags);
}
if (export_params.export_normals) {
obj_writer.write_poly_normals(*obj_mesh);
}
if (export_params.export_uv) {
obj_writer.write_uv_coords(*obj_mesh);
}
if (mtl_writer) {
obj_mtlindices = mtl_writer->add_materials(*obj_mesh);
}
/* This function takes a 0-indexed slot index for the obj_mesh object and
* returns the material name that we are using in the .obj file for it. */
std::function<const char *(int)> matname_fn = [&](int s) -> const char * {
if (!mtl_writer || s < 0 || s >= obj_mtlindices.size()) {
return nullptr;
}
return mtl_writer->mtlmaterial_name(obj_mtlindices[s]);
};
obj_writer.write_poly_elements(*obj_mesh, matname_fn);
}
obj_writer.write_edges_indices(*obj_mesh);
obj_writer.update_index_offsets(*obj_mesh);
}
}
/**
* Export NURBS Curves in parameter form, not as vertices and edges.
*/
static void write_nurbs_curve_objects(const Vector<std::unique_ptr<OBJCurve>> &exportable_as_nurbs,
const OBJWriter &obj_writer)
{
/* #OBJCurve doesn't have any dynamically allocated memory, so it's fine
* to wait for #blender::Vector to clean the objects up. */
for (const std::unique_ptr<OBJCurve> &obj_curve : exportable_as_nurbs) {
obj_writer.write_nurbs_curve(*obj_curve);
}
}
/**
* Export a single frame to a .OBJ file.
*
* Conditionally write a .MTL file also.
*/
void export_frame(Depsgraph *depsgraph, const OBJExportParams &export_params, const char *filepath)
{
std::unique_ptr<OBJWriter> frame_writer = nullptr;
try {
frame_writer = std::make_unique<OBJWriter>(filepath, export_params);
}
catch (const std::system_error &ex) {
print_exception_error(ex);
return;
}
if (!frame_writer) {
BLI_assert(!"File should be writable by now.");
return;
}
std::unique_ptr<MTLWriter> mtl_writer = nullptr;
if (export_params.export_materials) {
try {
mtl_writer = std::make_unique<MTLWriter>(export_params.filepath);
}
catch (const std::system_error &ex) {
print_exception_error(ex);
}
}
frame_writer->write_header();
auto [exportable_as_mesh, exportable_as_nurbs] = filter_supported_objects(depsgraph,
export_params);
write_mesh_objects(
std::move(exportable_as_mesh), *frame_writer, mtl_writer.get(), export_params);
if (mtl_writer) {
mtl_writer->write_header(export_params.blen_filepath);
mtl_writer->write_materials();
}
write_nurbs_curve_objects(std::move(exportable_as_nurbs), *frame_writer);
}
/**
* Append the current frame number in the .OBJ file name.
*
* \return Whether the filepath is in #FILE_MAX limits.
*/
bool append_frame_to_filename(const char *filepath, const int frame, char *r_filepath_with_frames)
{
BLI_strncpy(r_filepath_with_frames, filepath, FILE_MAX);
BLI_path_extension_replace(r_filepath_with_frames, FILE_MAX, "");
const int digits = frame == 0 ? 1 : integer_digits_i(abs(frame));
BLI_path_frame(r_filepath_with_frames, frame, digits);
return BLI_path_extension_replace(r_filepath_with_frames, FILE_MAX, ".obj");
}
/**
* Central internal function to call Scene update & writer functions.
*/
void exporter_main(bContext *C, const OBJExportParams &export_params)
{
ED_object_mode_set(C, OB_MODE_OBJECT);
OBJDepsgraph obj_depsgraph(C, export_params.export_eval_mode);
Scene *scene = DEG_get_input_scene(obj_depsgraph.get());
const char *filepath = export_params.filepath;
/* Single frame export, i.e. no animation. */
if (!export_params.export_animation) {
fprintf(stderr, "Writing to %s\n", filepath);
export_frame(obj_depsgraph.get(), export_params, filepath);
return;
}
char filepath_with_frames[FILE_MAX];
/* Used to reset the Scene to its original state. */
const int original_frame = CFRA;
for (int frame = export_params.start_frame; frame <= export_params.end_frame; frame++) {
const bool filepath_ok = append_frame_to_filename(filepath, frame, filepath_with_frames);
if (!filepath_ok) {
fprintf(stderr, "Error: File Path too long.\n%s\n", filepath_with_frames);
return;
}
CFRA = frame;
obj_depsgraph.update_for_newframe();
fprintf(stderr, "Writing to %s\n", filepath_with_frames);
export_frame(obj_depsgraph.get(), export_params, filepath_with_frames);
}
CFRA = original_frame;
}
} // namespace blender::io::obj

View File

@ -0,0 +1,88 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup obj
*/
#pragma once
#include "BLI_utility_mixins.hh"
#include "BLI_vector.hh"
#include "IO_wavefront_obj.h"
namespace blender::io::obj {
/**
* Behaves like `std::unique_ptr<Depsgraph, custom_deleter>`.
* Needed to free a new Depsgraph created for #DAG_EVAL_RENDER.
*/
class OBJDepsgraph : NonMovable, NonCopyable {
private:
Depsgraph *depsgraph_ = nullptr;
bool needs_free_ = false;
public:
OBJDepsgraph(const bContext *C, const eEvaluationMode eval_mode);
~OBJDepsgraph();
Depsgraph *get();
void update_for_newframe();
};
/**
* The main function for exporting a .obj file according to the given `export_parameters`.
* It uses the context `C` to get the dependency graph, and from that, the `Scene`.
* Depending on whether or not `export_params.export_animation` is set, it writes
* either one file per animation frame, or just one file.
*/
void exporter_main(bContext *C, const OBJExportParams &export_params);
class OBJMesh;
class OBJCurve;
/**
* Export a single frame of a .obj file, according to the given `export_parameters`.
* The frame state is given in `depsgraph`.
* The output file name is given by `filepath`.
* This function is normally called from `exporter_main`, but is exposed here for testing purposes.
*/
void export_frame(Depsgraph *depsgraph,
const OBJExportParams &export_params,
const char *filepath);
/**
* Find the objects to be exported in the `view_layer` of the dependency graph`depsgraph`,
* and return them in vectors `unique_ptr`s of `OBJMesh` and `OBJCurve`.
* If `export_params.export_selected_objects` is set, then only selected objects are to be
* exported, else all objects are to be exported. But only objects of type `OB_MESH`, `OB_CURVE`,
* and `OB_SURF` are supported; the rest will be ignored. If `export_params.export_curves_as_nurbs`
* is set, then curves of type `CU_NURBS` are exported in curve form in the .obj file, otherwise
* they are converted to mesh and returned in the `OBJMesh` vector. All other exportable types are
* always converted to mesh and returned in the `OBJMesh` vector.
*/
std::pair<Vector<std::unique_ptr<OBJMesh>>, Vector<std::unique_ptr<OBJCurve>>>
filter_supported_objects(Depsgraph *depsgraph, const OBJExportParams &export_params);
/**
* Makes `r_filepath_with_frames` (which should point at a character array of size `FILE_MAX`)
* be `filepath` with its "#" characters replaced by the number representing `frame`, and with
* a .obj extension.
*/
bool append_frame_to_filename(const char *filepath, const int frame, char *r_filepath_with_frames);
} // namespace blender::io::obj

View File

@ -0,0 +1,417 @@
/* Apache License, Version 2.0 */
#include <fstream>
#include <gtest/gtest.h>
#include <ios>
#include <memory>
#include <sstream>
#include <string>
#include <system_error>
#include "testing/testing.h"
#include "tests/blendfile_loading_base_test.h"
#include "BKE_appdir.h"
#include "BKE_blender_version.h"
#include "BLI_fileops.h"
#include "BLI_index_range.hh"
#include "BLI_string_utf8.h"
#include "BLI_vector.hh"
#include "DEG_depsgraph.h"
#include "obj_export_file_writer.hh"
#include "obj_export_mesh.hh"
#include "obj_export_nurbs.hh"
#include "obj_exporter.hh"
#include "obj_exporter_tests.hh"
namespace blender::io::obj {
/* This is also the test name. */
class obj_exporter_test : public BlendfileLoadingBaseTest {
public:
/**
* \param filepath: relative to "tests" directory.
*/
bool load_file_and_depsgraph(const std::string &filepath,
const eEvaluationMode eval_mode = DAG_EVAL_VIEWPORT)
{
if (!blendfile_load(filepath.c_str())) {
return false;
}
depsgraph_create(eval_mode);
return true;
}
};
const std::string all_objects_file = "io_tests/blend_scene/all_objects.blend";
const std::string all_curve_objects_file = "io_tests/blend_scene/all_curves.blend";
TEST_F(obj_exporter_test, filter_objects_curves_as_mesh)
{
OBJExportParamsDefault _export;
if (!load_file_and_depsgraph(all_objects_file)) {
ADD_FAILURE();
return;
}
auto [objmeshes, objcurves]{filter_supported_objects(depsgraph, _export.params)};
EXPECT_EQ(objmeshes.size(), 17);
EXPECT_EQ(objcurves.size(), 0);
}
TEST_F(obj_exporter_test, filter_objects_curves_as_nurbs)
{
OBJExportParamsDefault _export;
if (!load_file_and_depsgraph(all_objects_file)) {
ADD_FAILURE();
return;
}
_export.params.export_curves_as_nurbs = true;
auto [objmeshes, objcurves]{filter_supported_objects(depsgraph, _export.params)};
EXPECT_EQ(objmeshes.size(), 16);
EXPECT_EQ(objcurves.size(), 2);
}
TEST_F(obj_exporter_test, filter_objects_selected)
{
OBJExportParamsDefault _export;
if (!load_file_and_depsgraph(all_objects_file)) {
ADD_FAILURE();
return;
}
_export.params.export_selected_objects = true;
_export.params.export_curves_as_nurbs = true;
auto [objmeshes, objcurves]{filter_supported_objects(depsgraph, _export.params)};
EXPECT_EQ(objmeshes.size(), 1);
EXPECT_EQ(objcurves.size(), 0);
}
TEST(obj_exporter_utils, append_negative_frame_to_filename)
{
const char path_original[FILE_MAX] = "/my_file.obj";
const char path_truth[FILE_MAX] = "/my_file-123.obj";
const int frame = -123;
char path_with_frame[FILE_MAX] = {0};
const bool ok = append_frame_to_filename(path_original, frame, path_with_frame);
EXPECT_TRUE(ok);
EXPECT_EQ_ARRAY(path_with_frame, path_truth, BLI_strlen_utf8(path_truth));
}
TEST(obj_exporter_utils, append_positive_frame_to_filename)
{
const char path_original[FILE_MAX] = "/my_file.obj";
const char path_truth[FILE_MAX] = "/my_file123.obj";
const int frame = 123;
char path_with_frame[FILE_MAX] = {0};
const bool ok = append_frame_to_filename(path_original, frame, path_with_frame);
EXPECT_TRUE(ok);
EXPECT_EQ_ARRAY(path_with_frame, path_truth, BLI_strlen_utf8(path_truth));
}
TEST_F(obj_exporter_test, curve_nurbs_points)
{
if (!load_file_and_depsgraph(all_curve_objects_file)) {
ADD_FAILURE();
return;
}
OBJExportParamsDefault _export;
_export.params.export_curves_as_nurbs = true;
auto [objmeshes_unused, objcurves]{filter_supported_objects(depsgraph, _export.params)};
for (auto &objcurve : objcurves) {
if (all_nurbs_truth.count(objcurve->get_curve_name()) != 1) {
ADD_FAILURE();
return;
}
const NurbsObject *const nurbs_truth = all_nurbs_truth.at(objcurve->get_curve_name()).get();
EXPECT_EQ(objcurve->total_splines(), nurbs_truth->total_splines());
for (int spline_index : IndexRange(objcurve->total_splines())) {
EXPECT_EQ(objcurve->total_spline_vertices(spline_index),
nurbs_truth->total_spline_vertices(spline_index));
EXPECT_EQ(objcurve->get_nurbs_degree(spline_index),
nurbs_truth->get_nurbs_degree(spline_index));
EXPECT_EQ(objcurve->total_spline_control_points(spline_index),
nurbs_truth->total_spline_control_points(spline_index));
}
}
}
TEST_F(obj_exporter_test, curve_coordinates)
{
if (!load_file_and_depsgraph(all_curve_objects_file)) {
ADD_FAILURE();
return;
}
OBJExportParamsDefault _export;
_export.params.export_curves_as_nurbs = true;
auto [objmeshes_unused, objcurves]{filter_supported_objects(depsgraph, _export.params)};
for (auto &objcurve : objcurves) {
if (all_nurbs_truth.count(objcurve->get_curve_name()) != 1) {
ADD_FAILURE();
return;
}
const NurbsObject *const nurbs_truth = all_nurbs_truth.at(objcurve->get_curve_name()).get();
EXPECT_EQ(objcurve->total_splines(), nurbs_truth->total_splines());
for (int spline_index : IndexRange(objcurve->total_splines())) {
for (int vertex_index : IndexRange(objcurve->total_spline_vertices(spline_index))) {
EXPECT_V3_NEAR(objcurve->vertex_coordinates(
spline_index, vertex_index, _export.params.scaling_factor),
nurbs_truth->vertex_coordinates(spline_index, vertex_index),
0.000001f);
}
}
}
}
static std::unique_ptr<OBJWriter> init_writer(const OBJExportParams &params,
const std::string out_filepath)
{
try {
auto writer = std::make_unique<OBJWriter>(out_filepath.c_str(), params);
return writer;
}
catch (const std::system_error &ex) {
std::cerr << ex.code().category().name() << ": " << ex.what() << ": " << ex.code().message()
<< std::endl;
return nullptr;
}
}
/* The following is relative to BKE_tempdir_base. */
const char *const temp_file_path = "output.OBJ";
static std::string read_temp_file_in_string(const std::string &file_path)
{
std::ifstream temp_stream(file_path);
std::ostringstream input_ss;
input_ss << temp_stream.rdbuf();
return input_ss.str();
}
TEST(obj_exporter_writer, header)
{
/* Because testing doesn't fully initialize Blender, we need the following. */
BKE_tempdir_init(NULL);
std::string out_file_path = blender::tests::flags_test_release_dir() + "/" + temp_file_path;
{
OBJExportParamsDefault _export;
std::unique_ptr<OBJWriter> writer = init_writer(_export.params, out_file_path);
if (!writer) {
ADD_FAILURE();
return;
}
writer->write_header();
}
const std::string result = read_temp_file_in_string(out_file_path);
using namespace std::string_literals;
ASSERT_EQ(result, "# Blender "s + BKE_blender_version_string() + "\n" + "# www.blender.org\n");
BLI_delete(out_file_path.c_str(), false, false);
}
TEST(obj_exporter_writer, mtllib)
{
std::string out_file_path = blender::tests::flags_test_release_dir() + "/" + temp_file_path;
{
OBJExportParamsDefault _export;
std::unique_ptr<OBJWriter> writer = init_writer(_export.params, out_file_path);
if (!writer) {
ADD_FAILURE();
return;
}
writer->write_mtllib_name("/Users/blah.mtl");
writer->write_mtllib_name("\\C:\\blah.mtl");
}
const std::string result = read_temp_file_in_string(out_file_path);
ASSERT_EQ(result, "mtllib blah.mtl\nmtllib blah.mtl\n");
}
/* Return true if string #a and string #b are equal after their first newline. */
static bool strings_equal_after_first_lines(const std::string &a, const std::string &b)
{
/* If `dbg_level > 0` then a failing test will print context around the first mismatch. */
const bool dbg_level = 0;
const size_t a_len = a.size();
const size_t b_len = b.size();
const size_t a_next = a.find_first_of('\n');
const size_t b_next = b.find_first_of('\n');
if (a_next == std::string::npos || b_next == std::string::npos) {
if (dbg_level > 0) {
std::cout << "Couldn't find newline in one of args\n";
}
return false;
}
if (dbg_level > 0) {
if (a.compare(a_next, a_len - a_next, b, b_next, b_len - b_next) != 0) {
for (int i = 0; i < a_len - a_next && i < b_len - b_next; ++i) {
if (a[a_next + i] != b[b_next + i]) {
std::cout << "Difference found at pos " << a_next + i << " of a\n";
std::cout << "a: " << a.substr(a_next + i, 100) << " ...\n";
std::cout << "b: " << b.substr(b_next + i, 100) << " ... \n";
return false;
}
}
}
else {
return true;
}
}
return a.compare(a_next, a_len - a_next, b, b_next, b_len - b_next) == 0;
}
/* From here on, tests are whole file tests, testing for golden output. */
class obj_exporter_regression_test : public obj_exporter_test {
public:
/**
* Export the given blend file with the given parameters and
* test to see if it matches a golden file (ignoring any difference in Blender version number).
* \param blendfile: input, relative to "tests" directory.
* \param golden_obj: expected output, relative to "tests" directory.
* \param params: the parameters to be used for export.
*/
void compare_obj_export_to_golden(const std::string &blendfile,
const std::string &golden_obj,
const std::string &golden_mtl,
OBJExportParams &params)
{
if (!load_file_and_depsgraph(blendfile)) {
return;
}
/* Because testing doesn't fully initialize Blender, we need the following. */
BKE_tempdir_init(NULL);
std::string tempdir = std::string(BKE_tempdir_base());
std::string out_file_path = tempdir + BLI_path_basename(golden_obj.c_str());
strncpy(params.filepath, out_file_path.c_str(), FILE_MAX);
params.blen_filepath = blendfile.c_str();
export_frame(depsgraph, params, out_file_path.c_str());
std::string output_str = read_temp_file_in_string(out_file_path);
std::string golden_file_path = blender::tests::flags_test_asset_dir() + "/" + golden_obj;
std::string golden_str = read_temp_file_in_string(golden_file_path);
ASSERT_TRUE(strings_equal_after_first_lines(output_str, golden_str));
BLI_delete(out_file_path.c_str(), false, false);
if (!golden_mtl.empty()) {
std::string out_mtl_file_path = tempdir + BLI_path_basename(golden_mtl.c_str());
std::string output_mtl_str = read_temp_file_in_string(out_mtl_file_path);
std::string golden_mtl_file_path = blender::tests::flags_test_asset_dir() + "/" + golden_mtl;
std::string golden_mtl_str = read_temp_file_in_string(golden_mtl_file_path);
ASSERT_TRUE(strings_equal_after_first_lines(output_mtl_str, golden_mtl_str));
BLI_delete(out_mtl_file_path.c_str(), false, false);
}
}
};
TEST_F(obj_exporter_regression_test, all_tris)
{
OBJExportParamsDefault _export;
compare_obj_export_to_golden("io_tests/blend_geometry/all_tris.blend",
"io_tests/obj/all_tris.obj",
"io_tests/obj/all_tris.mtl",
_export.params);
}
TEST_F(obj_exporter_regression_test, all_quads)
{
OBJExportParamsDefault _export;
_export.params.scaling_factor = 2.0f;
_export.params.export_materials = false;
compare_obj_export_to_golden(
"io_tests/blend_geometry/all_quads.blend", "io_tests/obj/all_quads.obj", "", _export.params);
}
TEST_F(obj_exporter_regression_test, fgons)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.export_materials = false;
compare_obj_export_to_golden(
"io_tests/blend_geometry/fgons.blend", "io_tests/obj/fgons.obj", "", _export.params);
}
TEST_F(obj_exporter_regression_test, edges)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.export_materials = false;
compare_obj_export_to_golden(
"io_tests/blend_geometry/edges.blend", "io_tests/obj/edges.obj", "", _export.params);
}
TEST_F(obj_exporter_regression_test, vertices)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.export_materials = false;
compare_obj_export_to_golden(
"io_tests/blend_geometry/vertices.blend", "io_tests/obj/vertices.obj", "", _export.params);
}
TEST_F(obj_exporter_regression_test, nurbs_as_nurbs)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.export_materials = false;
_export.params.export_curves_as_nurbs = true;
compare_obj_export_to_golden(
"io_tests/blend_geometry/nurbs.blend", "io_tests/obj/nurbs.obj", "", _export.params);
}
TEST_F(obj_exporter_regression_test, nurbs_as_mesh)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.export_materials = false;
_export.params.export_curves_as_nurbs = false;
compare_obj_export_to_golden(
"io_tests/blend_geometry/nurbs.blend", "io_tests/obj/nurbs_mesh.obj", "", _export.params);
}
TEST_F(obj_exporter_regression_test, cube_all_data_triangulated)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.export_materials = false;
_export.params.export_triangulated_mesh = true;
compare_obj_export_to_golden("io_tests/blend_geometry/cube_all_data.blend",
"io_tests/obj/cube_all_data_triangulated.obj",
"",
_export.params);
}
TEST_F(obj_exporter_regression_test, suzanne_all_data)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.export_materials = false;
_export.params.export_smooth_groups = true;
compare_obj_export_to_golden("io_tests/blend_geometry/suzanne_all_data.blend",
"io_tests/obj/suzanne_all_data.obj",
"",
_export.params);
}
TEST_F(obj_exporter_regression_test, all_objects)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.export_smooth_groups = true;
compare_obj_export_to_golden("io_tests/blend_scene/all_objects.blend",
"io_tests/obj/all_objects.obj",
"io_tests/obj/all_objects.mtl",
_export.params);
}
} // namespace blender::io::obj

View File

@ -0,0 +1,149 @@
/* Apache License, Version 2.0 */
/**
* This file contains default values for several items like
* vertex coordinates, export parameters, MTL values etc.
*/
#pragma once
#include <array>
#include <gtest/gtest.h>
#include <string>
#include <vector>
#include "IO_wavefront_obj.h"
namespace blender::io::obj {
using array_float_3 = std::array<float, 3>;
/**
* This matches #OBJCurve's member functions, except that all the numbers and names are known
* constants. Used to store expected values of NURBS Curve sobjects.
*/
class NurbsObject {
private:
std::string nurbs_name_;
/* The indices in these vectors are spline indices. */
std::vector<std::vector<array_float_3>> coordinates_;
std::vector<int> degrees_;
std::vector<int> control_points_;
public:
NurbsObject(const std::string nurbs_name,
const std::vector<std::vector<array_float_3>> coordinates,
const std::vector<int> degrees,
const std::vector<int> control_points)
: nurbs_name_(nurbs_name),
coordinates_(coordinates),
degrees_(degrees),
control_points_(control_points)
{
}
int total_splines() const
{
return coordinates_.size();
}
int total_spline_vertices(const int spline_index) const
{
if (spline_index >= coordinates_.size()) {
ADD_FAILURE();
return 0;
}
return coordinates_[spline_index].size();
}
const float *vertex_coordinates(const int spline_index, const int vertex_index) const
{
return coordinates_[spline_index][vertex_index].data();
}
int get_nurbs_degree(const int spline_index) const
{
return degrees_[spline_index];
}
int total_spline_control_points(const int spline_index) const
{
return control_points_[spline_index];
}
};
struct OBJExportParamsDefault {
OBJExportParams params;
OBJExportParamsDefault()
{
params.filepath[0] = '\0';
params.blen_filepath = "";
params.export_animation = false;
params.start_frame = 0;
params.end_frame = 1;
params.forward_axis = OBJ_AXIS_NEGATIVE_Z_FORWARD;
params.up_axis = OBJ_AXIS_Y_UP;
params.scaling_factor = 1.f;
params.export_eval_mode = DAG_EVAL_VIEWPORT;
params.export_selected_objects = false;
params.export_uv = true;
params.export_normals = true;
params.export_materials = true;
params.export_triangulated_mesh = false;
params.export_curves_as_nurbs = false;
params.export_object_groups = false;
params.export_material_groups = false;
params.export_vertex_groups = false;
params.export_smooth_groups = true;
params.smooth_groups_bitflags = false;
}
};
const std::vector<std::vector<array_float_3>> coordinates_NurbsCurve{
{{6.94742, 0.000000, 0.000000},
{7.44742, 0.000000, -1.000000},
{9.44742, 0.000000, -1.000000},
{9.94742, 0.000000, 0.000000}}};
const std::vector<std::vector<array_float_3>> coordinates_NurbsCircle{
{{11.463165, 0.000000, 1.000000},
{10.463165, 0.000000, 1.000000},
{10.463165, 0.000000, 0.000000},
{10.463165, 0.000000, -1.000000},
{11.463165, 0.000000, -1.000000},
{12.463165, 0.000000, -1.000000},
{12.463165, 0.000000, 0.000000},
{12.463165, 0.000000, 1.000000}}};
const std::vector<std::vector<array_float_3>> coordinates_NurbsPathCurve{
{{13.690557, 0.000000, 0.000000},
{14.690557, 0.000000, 0.000000},
{15.690557, 0.000000, 0.000000},
{16.690557, 0.000000, 0.000000},
{17.690557, 0.000000, 0.000000}},
{{14.192808, 0.000000, 0.000000},
{14.692808, 0.000000, -1.000000},
{16.692808, 0.000000, -1.000000},
{17.192808, 0.000000, 0.000000}}};
const std::map<std::string, std::unique_ptr<NurbsObject>> all_nurbs_truth = []() {
std::map<std::string, std::unique_ptr<NurbsObject>> all_nurbs;
all_nurbs.emplace(
"NurbsCurve",
/* Name, coordinates, degrees of splines, control points of splines. */
std::make_unique<NurbsObject>(
"NurbsCurve", coordinates_NurbsCurve, std::vector<int>{3}, std::vector<int>{4}));
all_nurbs.emplace(
"NurbsCircle",
std::make_unique<NurbsObject>(
"NurbsCircle", coordinates_NurbsCircle, std::vector<int>{3}, std::vector<int>{11}));
/* This is actually an Object containing a NurbsPath and a NurbsCurve spline. */
all_nurbs.emplace("NurbsPathCurve",
std::make_unique<NurbsObject>("NurbsPathCurve",
coordinates_NurbsPathCurve,
std::vector<int>{3, 3},
std::vector<int>{5, 4}));
return all_nurbs;
}();
} // namespace blender::io::obj

View File

@ -184,6 +184,9 @@ void WM_operator_properties_filesel(wmOperatorType *ot,
prop = RNA_def_boolean(
ot->srna, "filter_usd", (filter & FILE_TYPE_USD) != 0, "Filter USD files", "");
RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
prop = RNA_def_boolean(
ot->srna, "filter_obj", (filter & FILE_TYPE_OBJECT_IO) != 0, "Filter OBJ files", "");
RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
prop = RNA_def_boolean(ot->srna,
"filter_volume",
(filter & FILE_TYPE_VOLUME) != 0,