Fix T96399: New 3.1 OBJ exporter is missing Path Mode setting

New OBJ exporter is missing "Path Mode" setting for exporting .mtl
files. The options that used to be available were: Auto, Absolute,
Relative, Match, Strip Path, Copy. All of them are important. The new
behavior (without any UI option to control it) curiously does not match
any of the previous setting. New behavior is like "Relative, but to the
source blender file, and not the destination export file".

Most of the previous logic was only present in Python based code
(bpy_extras.io_utils.path_reference and friends). The bulk of this
commit is porting that to C++.

Reviewed By: Howard Trickey
Differential Revision: https://developer.blender.org/D14906
This commit is contained in:
Aras Pranckevicius 2022-05-10 11:34:42 +03:00
parent 1dd1772419
commit 3bc037a7eb
Notes: blender-bot 2023-02-14 06:32:27 +01:00
Referenced by issue #96399, Regression: New 3.1 OBJ exporter is missing Path Mode setting
13 changed files with 226 additions and 23 deletions

View File

@ -9,6 +9,7 @@ set(INC
../../depsgraph
../../io/alembic
../../io/collada
../../io/common
../../io/gpencil
../../io/usd
../../io/wavefront_obj

View File

@ -29,6 +29,7 @@
#include "DEG_depsgraph.h"
#include "IO_path_util_types.h"
#include "IO_wavefront_obj.h"
#include "io_obj.h"
@ -59,6 +60,15 @@ static const EnumPropertyItem io_obj_export_evaluation_mode[] = {
"Export objects as they appear in the viewport"},
{0, NULL, 0, NULL, NULL}};
static const EnumPropertyItem io_obj_path_mode[] = {
{PATH_REFERENCE_AUTO, "AUTO", 0, "Auto", "Use Relative paths with subdirectories only"},
{PATH_REFERENCE_ABSOLUTE, "ABSOLUTE", 0, "Absolute", "Always write absolute paths"},
{PATH_REFERENCE_RELATIVE, "RELATIVE", 0, "Relative", "Write relative paths where possible"},
{PATH_REFERENCE_MATCH, "MATCH", 0, "Match", "Match Absolute/Relative setting with input path"},
{PATH_REFERENCE_STRIP, "STRIP", 0, "Strip", "Write filename only"},
{PATH_REFERENCE_COPY, "COPY", 0, "Copy", "Copy the file to the destination path"},
{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")) {
@ -87,6 +97,7 @@ static int wm_obj_export_exec(bContext *C, wmOperator *op)
return OPERATOR_CANCELLED;
}
struct OBJExportParams export_params;
export_params.file_base_for_tests[0] = '\0';
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");
@ -103,6 +114,7 @@ static int wm_obj_export_exec(bContext *C, wmOperator *op)
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.path_mode = RNA_enum_get(op->ptr, "path_mode");
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");
@ -119,9 +131,9 @@ static int wm_obj_export_exec(bContext *C, wmOperator *op)
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");
const bool export_materials = RNA_boolean_get(imfptr, "export_materials");
uiLayoutSetPropSep(layout, true);
uiLayoutSetPropDecorate(layout, false);
@ -150,6 +162,9 @@ static void ui_obj_export_settings(uiLayout *layout, PointerRNA *imfptr)
uiItemR(sub, imfptr, "export_selected_objects", 0, IFACE_("Selected Only"), ICON_NONE);
uiItemR(sub, imfptr, "apply_modifiers", 0, IFACE_("Apply Modifiers"), ICON_NONE);
uiItemR(sub, imfptr, "export_eval_mode", 0, IFACE_("Properties"), ICON_NONE);
sub = uiLayoutColumn(sub, false);
uiLayoutSetEnabled(sub, export_materials);
uiItemR(sub, imfptr, "path_mode", 0, IFACE_("Path Mode"), ICON_NONE);
/* Options for what to write. */
box = uiLayoutBox(layout);
@ -162,6 +177,7 @@ static void ui_obj_export_settings(uiLayout *layout, PointerRNA *imfptr)
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);
/* Grouping options. */
box = uiLayoutBox(layout);
uiItemL(box, IFACE_("Grouping"), ICON_GROUP);
col = uiLayoutColumn(box, false);
@ -322,6 +338,12 @@ void WM_OT_obj_export(struct wmOperatorType *ot)
"Export Materials",
"Export MTL library. There must be a Principled-BSDF node for image textures to "
"be exported to the MTL file");
RNA_def_enum(ot->srna,
"path_mode",
io_obj_path_mode,
PATH_REFERENCE_AUTO,
"Path Mode",
"Method used to reference paths");
RNA_def_boolean(ot->srna,
"export_triangulated_mesh",
false,

View File

@ -19,10 +19,13 @@ set(SRC
intern/dupli_parent_finder.cc
intern/dupli_persistent_id.cc
intern/object_identifier.cc
intern/path_util.cc
intern/string_utils.cc
IO_abstract_hierarchy_iterator.h
IO_dupli_persistent_id.hh
IO_path_util.hh
IO_path_util_types.h
IO_string_utils.hh
IO_types.h
intern/dupli_parent_finder.hh

View File

@ -0,0 +1,29 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_string_ref.hh"
#include "BLI_set.hh"
#include "IO_path_util_types.h"
namespace blender::io {
/**
* Return a filepath relative to a destination directory, for use with
* exporters.
*
* When PATH_REFERENCE_COPY mode is used, the file path pair (source
* path, destination path) is added to the `copy_set`.
*
* Equivalent of bpy_extras.io_utils.path_reference.
*/
std::string path_reference(StringRefNull filepath,
StringRefNull base_src,
StringRefNull base_dst,
ePathReferenceMode mode,
Set<std::pair<std::string, std::string>> *copy_set = nullptr);
/** Execute copying files of path_reference. */
void path_reference_copy(const Set<std::pair<std::string, std::string>> &copy_set);
} // namespace blender::io

View File

@ -0,0 +1,18 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** Method used to reference paths. Equivalent of bpy_extras.io_utils.path_reference_mode. */
typedef enum {
/** Use Relative paths with subdirectories only. */
PATH_REFERENCE_AUTO = 0,
/** Always write absolute paths. */
PATH_REFERENCE_ABSOLUTE = 1,
/** Write relative paths where possible. */
PATH_REFERENCE_RELATIVE = 2,
/** Match Absolute/Relative setting with input path. */
PATH_REFERENCE_MATCH = 3,
/** Filename only. */
PATH_REFERENCE_STRIP = 4,
/** Copy the file to the destination path. */
PATH_REFERENCE_COPY = 5,
} ePathReferenceMode;

View File

@ -0,0 +1,81 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "IO_path_util.hh"
#include "BLI_fileops.h"
#include "BLI_path_util.h"
namespace blender::io {
std::string path_reference(StringRefNull filepath,
StringRefNull base_src,
StringRefNull base_dst,
ePathReferenceMode mode,
Set<std::pair<std::string, std::string>> *copy_set)
{
const bool is_relative = BLI_path_is_rel(filepath.c_str());
char filepath_abs[PATH_MAX];
BLI_strncpy(filepath_abs, filepath.c_str(), PATH_MAX);
BLI_path_abs(filepath_abs, base_src.c_str());
BLI_path_normalize(nullptr, filepath_abs);
/* Figure out final mode to be used. */
if (mode == PATH_REFERENCE_MATCH) {
mode = is_relative ? PATH_REFERENCE_RELATIVE : PATH_REFERENCE_ABSOLUTE;
}
else if (mode == PATH_REFERENCE_AUTO) {
mode = BLI_path_contains(base_dst.c_str(), filepath_abs) ? PATH_REFERENCE_RELATIVE :
PATH_REFERENCE_ABSOLUTE;
}
else if (mode == PATH_REFERENCE_COPY) {
char filepath_cpy[PATH_MAX];
BLI_path_join(filepath_cpy, PATH_MAX, base_dst.c_str(), BLI_path_basename(filepath_abs), nullptr);
copy_set->add(std::make_pair(filepath_abs, filepath_cpy));
BLI_strncpy(filepath_abs, filepath_cpy, PATH_MAX);
mode = PATH_REFERENCE_RELATIVE;
}
/* Now we know the final path mode. */
if (mode == PATH_REFERENCE_ABSOLUTE) {
return filepath_abs;
}
else if (mode == PATH_REFERENCE_RELATIVE) {
char rel_path[PATH_MAX];
BLI_strncpy(rel_path, filepath_abs, PATH_MAX);
BLI_path_rel(rel_path, base_dst.c_str());
/* Can't always find relative path (e.g. between different drives). */
if (!BLI_path_is_rel(rel_path)) {
return filepath_abs;
}
return rel_path + 2; /* Skip blender's internal "//" prefix. */
}
else if (mode == PATH_REFERENCE_STRIP) {
return BLI_path_basename(filepath_abs);
}
BLI_assert_msg(false, "Invalid path reference mode");
return filepath_abs;
}
void path_reference_copy(const Set<std::pair<std::string, std::string>> &copy_set)
{
for (const auto &copy : copy_set) {
const char *src = copy.first.c_str();
const char *dst = copy.second.c_str();
if (!BLI_exists(src)) {
fprintf(stderr, "Missing source file '%s', not copying\n", src);
continue;
}
if (0 == BLI_path_cmp_normalized(src, dst)) {
continue; /* Source and dest are the same. */
}
if (!BLI_make_existing_file(dst)) {
fprintf(stderr, "Can't make directory for '%s', not copying\n", dst);
continue;
}
if (!BLI_copy(src, dst)) {
fprintf(stderr, "Can't copy '%s' to '%s'\n", src, dst);
continue;
}
}
}
} // namespace blender::io

View File

@ -9,6 +9,7 @@
#include "BKE_context.h"
#include "BLI_path_util.h"
#include "DEG_depsgraph.h"
#include "IO_path_util_types.h"
#ifdef __cplusplus
extern "C" {
@ -37,6 +38,8 @@ static const int TOTAL_AXES = 3;
struct OBJExportParams {
/** Full path to the destination .OBJ file. */
char filepath[FILE_MAX];
/** Pretend that destination file folder is this, if non-empty. Used only for tests. */
char file_base_for_tests[FILE_MAX];
/** Full path to current blender file (used for comments in output). */
const char *blen_filepath;
@ -62,6 +65,7 @@ struct OBJExportParams {
bool export_materials;
bool export_triangulated_mesh;
bool export_curves_as_nurbs;
ePathReferenceMode path_mode;
/* Grouping options. */
bool export_object_groups;

View File

@ -13,6 +13,8 @@
#include "BLI_path_util.h"
#include "BLI_task.hh"
#include "IO_path_util.hh"
#include "obj_export_mesh.hh"
#include "obj_export_mtl.hh"
#include "obj_export_nurbs.hh"
@ -530,7 +532,11 @@ void MTLWriter::write_bsdf_properties(const MTLMaterial &mtl_material)
void MTLWriter::write_texture_map(
const MTLMaterial &mtl_material,
const Map<const eMTLSyntaxElement, tex_map_XX>::Item &texture_map)
const Map<const eMTLSyntaxElement, tex_map_XX>::Item &texture_map,
const char *blen_filedir,
const char *dest_dir,
ePathReferenceMode path_mode,
Set<std::pair<std::string, std::string>> &copy_set)
{
std::string options;
/* Option strings should have their own leading spaces. */
@ -546,7 +552,11 @@ void MTLWriter::write_texture_map(
#define SYNTAX_DISPATCH(eMTLSyntaxElement) \
if (texture_map.key == eMTLSyntaxElement) { \
fmt_handler_.write<eMTLSyntaxElement>(options, texture_map.value.image_path); \
std::string path = path_reference( \
texture_map.value.image_path.c_str(), blen_filedir, dest_dir, path_mode, &copy_set); \
/* Always emit forward slashes for cross-platform compatibility. */ \
std::replace(path.begin(), path.end(), '\\', '/'); \
fmt_handler_.write<eMTLSyntaxElement>(options, path.c_str()); \
return; \
}
@ -561,25 +571,35 @@ void MTLWriter::write_texture_map(
BLI_assert(!"This map type was not written to the file.");
}
void MTLWriter::write_materials()
void MTLWriter::write_materials(const char *blen_filepath,
ePathReferenceMode path_mode,
const char *dest_dir)
{
if (mtlmaterials_.size() == 0) {
return;
}
char blen_filedir[PATH_MAX];
BLI_split_dir_part(blen_filepath, blen_filedir, PATH_MAX);
BLI_path_slash_native(blen_filedir);
BLI_path_normalize(nullptr, blen_filedir);
std::sort(mtlmaterials_.begin(),
mtlmaterials_.end(),
[](const MTLMaterial &a, const MTLMaterial &b) { return a.name < b.name; });
Set<std::pair<std::string, std::string>> copy_set;
for (const MTLMaterial &mtlmat : mtlmaterials_) {
fmt_handler_.write<eMTLSyntaxElement::string>("\n");
fmt_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);
for (const auto &tex : mtlmat.texture_maps.items()) {
if (tex.value.image_path.empty()) {
continue;
}
write_texture_map(mtlmat, tex, blen_filedir, dest_dir, path_mode, copy_set);
}
}
path_reference_copy(copy_set);
}
Vector<int> MTLWriter::add_materials(const OBJMesh &mesh_to_export)

View File

@ -9,6 +9,7 @@
#include "DNA_meshdata_types.h"
#include "BLI_map.hh"
#include "BLI_set.hh"
#include "BLI_vector.hh"
#include "IO_wavefront_obj.h"
@ -181,7 +182,9 @@ class MTLWriter : NonMovable, NonCopyable {
* For consistency of output from run to run (useful for testing),
* the materials are sorted by name before writing.
*/
void write_materials();
void write_materials(const char *blen_filepath,
ePathReferenceMode path_mode,
const char *dest_dir);
StringRefNull mtl_file_path() const;
/**
* Add the materials of the given object to #MTLWriter, de-duplicating
@ -203,6 +206,10 @@ class MTLWriter : NonMovable, NonCopyable {
* Write a texture map in the form "map_XX -s 1. 1. 1. -o 0. 0. 0. [-bm 1.] path/to/image".
*/
void write_texture_map(const MTLMaterial &mtl_material,
const Map<const eMTLSyntaxElement, tex_map_XX>::Item &texture_map);
const Map<const eMTLSyntaxElement, tex_map_XX>::Item &texture_map,
const char *blen_filedir,
const char *dest_dir,
ePathReferenceMode mode,
Set<std::pair<std::string, std::string>> &copy_set);
};
} // namespace blender::io::obj

View File

@ -113,8 +113,7 @@ static const bNode *get_node_of_type(Span<const nodes::OutputSocketRef *> socket
/**
* 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.
* If packed image is found, only the file "name" is returned.
*/
static const char *get_image_filepath(const bNode *tex_node)
{
@ -134,9 +133,6 @@ static const char *get_image_filepath(const bNode *tex_node)
"directory as the .MTL file.\n",
path);
}
if (path[0] == '/' && path[1] == '/') {
path += 2;
}
return path;
}

View File

@ -284,7 +284,16 @@ void export_frame(Depsgraph *depsgraph, const OBJExportParams &export_params, co
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();
char dest_dir[PATH_MAX];
if (export_params.file_base_for_tests[0] == '\0') {
BLI_split_dir_part(export_params.filepath, dest_dir, PATH_MAX);
}
else {
BLI_strncpy(dest_dir, export_params.file_base_for_tests, PATH_MAX);
}
BLI_path_slash_native(dest_dir);
BLI_path_normalize(nullptr, dest_dir);
mtl_writer->write_materials(export_params.blen_filepath, export_params.path_mode, dest_dir);
}
write_nurbs_curve_objects(std::move(exportable_as_nurbs), *frame_writer);
}

View File

@ -11,12 +11,15 @@
#include "BKE_appdir.h"
#include "BKE_blender_version.h"
#include "BKE_main.h"
#include "BLI_fileops.h"
#include "BLI_index_range.hh"
#include "BLI_string_utf8.h"
#include "BLI_vector.hh"
#include "BLO_readfile.h"
#include "DEG_depsgraph.h"
#include "obj_export_file_writer.hh"
@ -259,11 +262,12 @@ class obj_exporter_regression_test : public obj_exporter_test {
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 - 1);
params.blen_filepath = blendfile.c_str();
params.blen_filepath = bfile->main->filepath;
std::string golden_file_path = blender::tests::flags_test_asset_dir() + "/" + golden_obj;
BLI_split_dir_part(golden_file_path.c_str(), params.file_base_for_tests, PATH_MAX);
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);
bool are_equal = strings_equal_after_first_lines(output_str, golden_str);
if (save_failing_test_output && !are_equal) {
@ -432,19 +436,26 @@ TEST_F(obj_exporter_regression_test, cubes_positioned)
_export.params);
}
/* Note: texture paths in the resulting mtl file currently are always
* as they are stored in the source .blend file; not relative to where
* the export is done. When that is properly fixed, the expected .mtl
* file should be updated. */
TEST_F(obj_exporter_regression_test, cubes_with_textures)
TEST_F(obj_exporter_regression_test, cubes_with_textures_strip)
{
OBJExportParamsDefault _export;
_export.params.path_mode = PATH_REFERENCE_STRIP;
compare_obj_export_to_golden("io_tests/blend_geometry/cubes_with_textures.blend",
"io_tests/obj/cubes_with_textures.obj",
"io_tests/obj/cubes_with_textures.mtl",
_export.params);
}
TEST_F(obj_exporter_regression_test, cubes_with_textures_relative)
{
OBJExportParamsDefault _export;
_export.params.path_mode = PATH_REFERENCE_RELATIVE;
compare_obj_export_to_golden("io_tests/blend_geometry/cubes_with_textures.blend",
"io_tests/obj/cubes_with_textures_rel.obj",
"io_tests/obj/cubes_with_textures_rel.mtl",
_export.params);
}
TEST_F(obj_exporter_regression_test, suzanne_all_data)
{
OBJExportParamsDefault _export;

View File

@ -11,6 +11,7 @@ struct OBJExportParamsDefault {
OBJExportParamsDefault()
{
params.filepath[0] = '\0';
params.file_base_for_tests[0] = '\0';
params.blen_filepath = "";
params.export_animation = false;
params.start_frame = 0;
@ -26,6 +27,7 @@ struct OBJExportParamsDefault {
params.export_uv = true;
params.export_normals = true;
params.export_materials = true;
params.path_mode = PATH_REFERENCE_AUTO;
params.export_triangulated_mesh = false;
params.export_curves_as_nurbs = false;