Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
set(INC
.
exporter
importer
../common
../../bmesh/intern
../../editors/include
../../makesrna
)
set(INC_SYS
)
set(SRC
IO_wavefront_obj.cc
exporter/obj_export_file_writer.cc
exporter/obj_export_mesh.cc
exporter/obj_export_mtl.cc
exporter/obj_export_nurbs.cc
exporter/obj_exporter.cc
importer/importer_mesh_utils.cc
importer/obj_import_file_reader.cc
importer/obj_import_mesh.cc
importer/obj_import_mtl.cc
importer/obj_import_nurbs.cc
importer/obj_importer.cc
IO_wavefront_obj.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
exporter/obj_exporter.hh
importer/importer_mesh_utils.hh
importer/obj_import_file_reader.hh
importer/obj_import_mesh.hh
importer/obj_import_mtl.hh
importer/obj_import_nurbs.hh
importer/obj_import_objects.hh
importer/obj_importer.hh
)
set(LIB
PRIVATE bf::blenkernel
PRIVATE bf::blenlib
PRIVATE bf::bmesh
PRIVATE bf::depsgraph
PRIVATE bf::dna
PRIVATE bf::imbuf
PRIVATE bf::intern::clog
PRIVATE bf::intern::guardedalloc
bf_io_common
PRIVATE bf::extern::fast_float
PRIVATE bf::nodes
PRIVATE bf::windowmanager
)
blender_add_lib(bf_io_wavefront_obj "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
if(WITH_GTESTS)
set(TEST_SRC
tests/obj_exporter_tests.cc
tests/obj_mtl_parser_tests.cc
tests/obj_nurbs_io_tests.cc
)
set(TEST_INC
${INC}
../../blenloader
../../../../tests/gtests
)
set(TEST_LIB
${LIB}
bf_blenloader_test_util
bf_io_wavefront_obj
)
blender_add_test_suite_lib(io_wavefront "${TEST_SRC}" "${TEST_INC}" "${INC_SYS}" "${TEST_LIB}")
endif()

View File

@@ -0,0 +1,51 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include "BLI_path_utils.hh"
#include "BLI_timeit.hh"
#include "IO_wavefront_obj.hh"
#include "obj_exporter.hh"
#include "obj_importer.hh"
#include <fmt/core.h>
namespace blender {
using namespace blender::timeit;
static void report_duration(const char *job, const TimePoint &start_time, const char *path)
{
Nanoseconds duration = Clock::now() - start_time;
fmt::print("OBJ {} of '{}' took ", job, BLI_path_basename(path));
print_duration(duration);
fmt::print("\n");
}
void OBJ_export(bContext *C, const OBJExportParams *export_params)
{
TimePoint start_time = Clock::now();
io::obj::exporter_main(C, *export_params);
report_duration("export", start_time, export_params->filepath);
}
void OBJ_import(bContext *C, const OBJImportParams *import_params)
{
TimePoint start_time = Clock::now();
io::obj::importer_main(C, *import_params);
report_duration("import", start_time, import_params->filepath);
}
void OBJ_import_geometries(const OBJImportParams *import_params,
Vector<bke::GeometrySet> &geometries)
{
io::obj::importer_geometry(*import_params, geometries);
}
} // namespace blender

View File

@@ -0,0 +1,125 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include <limits.h>
#include "BLI_path_utils.hh"
#include "BKE_geometry_set.hh"
#include "DEG_depsgraph.hh"
#include "IO_orientation.hh"
#include "IO_path_util_types.hh"
namespace blender {
struct bContext;
struct ReportList;
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] = "";
char collection[MAX_ID_NAME - 2] = "";
/** Full path to current blender file (used for comments in output). */
const char *blen_filepath = nullptr;
/** Whether multiple frames should be exported. */
bool export_animation = false;
/** The first frame to be exported. */
int start_frame = INT_MIN;
/** The last frame to be exported. */
int end_frame = INT_MAX;
/* Geometry Transform options. */
eIOAxis forward_axis = IO_AXIS_NEGATIVE_Z;
eIOAxis up_axis = IO_AXIS_Y;
float global_scale = 1.0f;
/* File Write Options. */
bool export_selected_objects = false;
bool apply_modifiers = true;
bool apply_transform = true;
eEvaluationMode export_eval_mode = DAG_EVAL_VIEWPORT;
bool export_uv = true;
bool export_normals = true;
bool export_colors = false;
bool export_materials = true;
bool export_triangulated_mesh = false;
bool export_curves_as_nurbs = false;
ePathReferenceMode path_mode = PATH_REFERENCE_AUTO;
bool export_pbr_extensions = false;
/* Grouping options. */
bool export_object_groups = false;
bool export_material_groups = false;
bool export_vertex_groups = false;
/* Calculate smooth groups from sharp edges. */
bool export_smooth_groups = false;
/* Create bitflags instead of the default "0"/"1" group IDs. */
bool smooth_groups_bitflags = false;
ReportList *reports = nullptr;
};
/**
* Behavior when the name of an imported material
* conflicts with an existing material.
*/
enum eOBJMtlNameCollisionMode {
OBJ_MTL_NAME_COLLISION_MAKE_UNIQUE = 0,
OBJ_MTL_NAME_COLLISION_REFERENCE_EXISTING = 1,
};
struct OBJImportParams {
/** Full path to the source OBJ file to import. */
char filepath[FILE_MAX] = "";
/** Value 0 disables clamping. */
float clamp_size = 0.0f;
float global_scale = 1.0f;
eIOAxis forward_axis = IO_AXIS_NEGATIVE_Z;
eIOAxis up_axis = IO_AXIS_Y;
char collection_separator = 0;
bool use_split_objects = true;
bool use_split_groups = false;
bool import_vertex_groups = false;
bool validate_meshes = true;
bool close_spline_loops = true;
bool relative_paths = true;
bool clear_selection = true;
/** How to handle material name collisions during import. */
eOBJMtlNameCollisionMode mtl_name_collision_mode = OBJ_MTL_NAME_COLLISION_MAKE_UNIQUE;
ReportList *reports = nullptr;
};
/**
* Reads and returns just the meshes in the obj file
*/
void OBJ_import_geometries(const OBJImportParams *import_params,
Vector<bke::GeometrySet> &geometries);
/**
* Perform the full import process.
* Import also changes the selection & the active object; callers
* need to update the UI bits if needed.
*/
void OBJ_import(bContext *C, const OBJImportParams *import_params);
/**
* Perform the full export process.
*/
void OBJ_export(bContext *C, const OBJExportParams *export_params);
} // namespace blender

View File

@@ -0,0 +1,803 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include <algorithm>
#include <system_error>
#include "BKE_attribute.hh"
#include "BKE_blender_version.h"
#include "BKE_mesh.hh"
#include "BLI_color_types.hh"
#include "BLI_enumerable_thread_specific.hh"
#include "BLI_fileops.h"
#include "BLI_math_color.h"
#include "BLI_math_matrix.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_rotation.h"
#include "BLI_math_vector.h"
#include "BLI_path_utils.hh"
#include "BLI_string.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"
#include "obj_export_file_writer.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.obj"};
namespace 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;
static 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. */
static const char *MATERIAL_GROUP_DISABLED = "";
OBJWriter::OBJWriter(const char *filepath, const OBJExportParams &export_params) noexcept(false)
: export_params_(export_params), outfile_path_(filepath), outfile_(nullptr)
{
outfile_ = BLI_fopen(filepath, "wb");
if (!outfile_) {
throw std::system_error(errno, std::system_category(), "Cannot open file " + outfile_path_);
}
}
OBJWriter::~OBJWriter()
{
if (outfile_ && std::fclose(outfile_)) {
CLOG_ERROR(&LOG,
"Error: could not close file '%s' properly, it may be corrupted.",
outfile_path_.c_str());
}
}
void OBJWriter::write_vert_uv_normal_indices(FormatHandler &fh,
const IndexOffsets &offsets,
Span<int> vert_indices,
Span<int> uv_indices,
Span<int> normal_indices,
bool flip) const
{
BLI_assert(vert_indices.size() == uv_indices.size() &&
vert_indices.size() == normal_indices.size());
const int vertex_offset = offsets.vertex_offset + 1;
const int uv_offset = offsets.uv_vertex_offset + 1;
const int normal_offset = offsets.normal_offset + 1;
const int n = vert_indices.size();
fh.write_obj_face_begin();
if (!flip) {
for (int j = 0; j < n; ++j) {
fh.write_obj_face_v_uv_normal(vert_indices[j] + vertex_offset,
uv_indices[j] + uv_offset,
normal_indices[j] + normal_offset);
}
}
else {
/* For a transform that is mirrored (negative scale on odd number of axes),
* we want to flip the face index order. Start from the same index, and
* then go backwards. Same logic in other write_*_indices functions below. */
for (int k = 0; k < n; ++k) {
int j = k == 0 ? 0 : n - k;
fh.write_obj_face_v_uv_normal(vert_indices[j] + vertex_offset,
uv_indices[j] + uv_offset,
normal_indices[j] + normal_offset);
}
}
fh.write_obj_face_end();
}
void OBJWriter::write_vert_normal_indices(FormatHandler &fh,
const IndexOffsets &offsets,
Span<int> vert_indices,
Span<int> /*uv_indices*/,
Span<int> normal_indices,
bool flip) const
{
BLI_assert(vert_indices.size() == normal_indices.size());
const int vertex_offset = offsets.vertex_offset + 1;
const int normal_offset = offsets.normal_offset + 1;
const int n = vert_indices.size();
fh.write_obj_face_begin();
if (!flip) {
for (int j = 0; j < n; ++j) {
fh.write_obj_face_v_normal(vert_indices[j] + vertex_offset,
normal_indices[j] + normal_offset);
}
}
else {
for (int k = 0; k < n; ++k) {
int j = k == 0 ? 0 : n - k;
fh.write_obj_face_v_normal(vert_indices[j] + vertex_offset,
normal_indices[j] + normal_offset);
}
}
fh.write_obj_face_end();
}
void OBJWriter::write_vert_uv_indices(FormatHandler &fh,
const IndexOffsets &offsets,
Span<int> vert_indices,
Span<int> uv_indices,
Span<int> /*normal_indices*/,
bool flip) const
{
BLI_assert(vert_indices.size() == uv_indices.size());
const int vertex_offset = offsets.vertex_offset + 1;
const int uv_offset = offsets.uv_vertex_offset + 1;
const int n = vert_indices.size();
fh.write_obj_face_begin();
if (!flip) {
for (int j = 0; j < n; ++j) {
fh.write_obj_face_v_uv(vert_indices[j] + vertex_offset, uv_indices[j] + uv_offset);
}
}
else {
for (int k = 0; k < n; ++k) {
int j = k == 0 ? 0 : n - k;
fh.write_obj_face_v_uv(vert_indices[j] + vertex_offset, uv_indices[j] + uv_offset);
}
}
fh.write_obj_face_end();
}
void OBJWriter::write_vert_indices(FormatHandler &fh,
const IndexOffsets &offsets,
Span<int> vert_indices,
Span<int> /*uv_indices*/,
Span<int> /*normal_indices*/,
bool flip) const
{
const int vertex_offset = offsets.vertex_offset + 1;
const int n = vert_indices.size();
fh.write_obj_face_begin();
if (!flip) {
for (int j = 0; j < n; ++j) {
fh.write_obj_face_v(vert_indices[j] + vertex_offset);
}
}
else {
for (int k = 0; k < n; ++k) {
int j = k == 0 ? 0 : n - k;
fh.write_obj_face_v(vert_indices[j] + vertex_offset);
}
}
fh.write_obj_face_end();
}
void OBJWriter::write_header() const
{
using namespace std::string_literals;
FormatHandler fh;
fh.write_string("# Blender "s + BKE_blender_version_string());
fh.write_string("# www.blender.org");
fh.write_to_file(outfile_);
}
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_path_split_dir_file(mtl_filepath.data(),
mtl_dir_name,
sizeof(mtl_dir_name),
mtl_file_name,
sizeof(mtl_file_name));
FormatHandler fh;
fh.write_obj_mtllib(mtl_file_name);
fh.write_to_file(outfile_);
}
static void spaces_to_underscores(std::string &r_name)
{
std::replace(r_name.begin(), r_name.end(), ' ', '_');
}
void OBJWriter::write_object_name(FormatHandler &fh, const OBJMesh &obj_mesh_data) const
{
std::string object_name = obj_mesh_data.get_object_name();
spaces_to_underscores(object_name);
if (export_params_.export_object_groups) {
std::string mesh_name = obj_mesh_data.get_object_mesh_name();
spaces_to_underscores(mesh_name);
fh.write_obj_group(object_name + "_" + mesh_name);
return;
}
fh.write_obj_object(object_name);
}
/* Split up large meshes into multi-threaded jobs; each job processes
* this amount of items. */
static const int chunk_size = 32768;
static int calc_chunk_count(int count)
{
return (count + chunk_size - 1) / chunk_size;
}
/* Write /tot_count/ items to OBJ file output. Each item is written
* by a /function/ that should be independent from other items.
* If the amount of items is large enough (> chunk_size), then writing
* will be done in parallel, into temporary FormatHandler buffers that
* will be written into the final /fh/ buffer at the end.
*/
template<typename Function>
void obj_parallel_chunked_output(FormatHandler &fh, int tot_count, const Function &function)
{
if (tot_count <= 0) {
return;
}
/* If we have just one chunk, process it directly into the output
* buffer - avoids all the job scheduling and temporary vector allocation
* overhead. */
const int chunk_count = calc_chunk_count(tot_count);
if (chunk_count == 1) {
for (int i = 0; i < tot_count; i++) {
function(fh, i);
}
return;
}
/* Give each chunk its own temporary output buffer, and process them in parallel. */
Array<FormatHandler> buffers(chunk_count);
threading::parallel_for(IndexRange(chunk_count), 1, [&](IndexRange range) {
for (const int r : range) {
int i_start = r * chunk_size;
int i_end = std::min(i_start + chunk_size, tot_count);
auto &buf = buffers[r];
for (int i = i_start; i < i_end; i++) {
function(buf, i);
}
}
});
/* Emit all temporary output buffers into the destination buffer. */
for (auto &buf : buffers) {
fh.append_from(buf);
}
}
void OBJWriter::write_vertex_coords(FormatHandler &fh,
const OBJMesh &obj_mesh_data,
bool write_colors) const
{
const int tot_count = obj_mesh_data.tot_vertices();
const Mesh *mesh = obj_mesh_data.get_mesh();
const StringRef name = mesh->active_color_attribute;
const float4x4 transform = obj_mesh_data.get_world_axes_transform();
const Span<float3> positions = obj_mesh_data.get_mesh()->vert_positions();
if (write_colors && !name.is_empty()) {
const bke::AttributeAccessor attributes = mesh->attributes();
const VArray<ColorGeometry4f> attribute = *attributes.lookup_or_default<ColorGeometry4f>(
name, bke::AttrDomain::Point, {0.0f, 0.0f, 0.0f, 0.0f});
BLI_assert(tot_count == attribute.size());
obj_parallel_chunked_output(fh, tot_count, [&](FormatHandler &buf, int i) {
const float3 vertex = math::transform_point(transform, positions[i]);
ColorGeometry4f linear = attribute.get(i);
float srgb[3];
linearrgb_to_srgb_v3_v3(srgb, linear);
buf.write_obj_vertex_color(vertex[0], vertex[1], vertex[2], srgb[0], srgb[1], srgb[2]);
});
}
else {
obj_parallel_chunked_output(fh, tot_count, [&](FormatHandler &buf, int i) {
const float3 vertex = math::transform_point(transform, positions[i]);
buf.write_obj_vertex(vertex[0], vertex[1], vertex[2]);
});
}
}
void OBJWriter::write_uv_coords(FormatHandler &fh, OBJMesh &r_obj_mesh_data) const
{
const Span<float2> uv_coords = r_obj_mesh_data.get_uv_coords();
obj_parallel_chunked_output(fh, uv_coords.size(), [&](FormatHandler &buf, int i) {
const float2 &uv_vertex = uv_coords[i];
buf.write_obj_uv(uv_vertex[0], uv_vertex[1]);
});
}
void OBJWriter::write_normals(FormatHandler &fh, OBJMesh &obj_mesh_data)
{
/* Poly normals should be calculated earlier via store_normal_coords_and_indices. */
const Span<float3> normal_coords = obj_mesh_data.get_normal_coords();
obj_parallel_chunked_output(fh, normal_coords.size(), [&](FormatHandler &buf, int i) {
const float3 &normal = normal_coords[i];
buf.write_obj_normal(normal[0], normal[1], normal[2]);
});
}
OBJWriter::func_vert_uv_normal_indices OBJWriter::get_face_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;
}
static int get_smooth_group(const OBJMesh &mesh, const OBJExportParams &params, int face_idx)
{
if (face_idx < 0) {
return NEGATIVE_INIT;
}
int group = SMOOTH_GROUP_DISABLED;
if (mesh.is_ith_face_smooth(face_idx)) {
group = !params.export_smooth_groups ? SMOOTH_GROUP_DEFAULT : mesh.ith_smooth_group(face_idx);
}
return group;
}
void OBJWriter::write_face_elements(FormatHandler &fh,
const IndexOffsets &offsets,
const OBJMesh &obj_mesh_data,
FunctionRef<const char *(int)> matname_fn)
{
const func_vert_uv_normal_indices face_element_writer = get_face_element_writer(
obj_mesh_data.tot_uv_vertices());
const int tot_faces = obj_mesh_data.tot_faces();
const int tot_deform_groups = obj_mesh_data.tot_deform_groups();
threading::EnumerableThreadSpecific<Vector<float>> group_weights;
const bke::AttributeAccessor attributes = obj_mesh_data.get_mesh()->attributes();
const VArray<int> material_indices = *attributes.lookup_or_default<int>(
"material_index", bke::AttrDomain::Face, 0);
obj_parallel_chunked_output(fh, tot_faces, [&](FormatHandler &buf, int idx) {
/* Polygon order for writing into the file is not necessarily the same
* as order in the mesh; it will be sorted by material indices. Remap current
* and previous indices here according to the order. */
int prev_i = obj_mesh_data.remap_face_index(idx - 1);
int i = obj_mesh_data.remap_face_index(idx);
const Span<int> face_vertex_indices = obj_mesh_data.calc_face_vert_indices(i);
const Span<int> face_uv_indices = obj_mesh_data.get_face_uv_indices(i);
const Span<int> face_normal_indices = obj_mesh_data.get_face_normal_indices(i);
/* Write smoothing group if different from previous. */
{
const int prev_group = get_smooth_group(obj_mesh_data, export_params_, prev_i);
const int group = get_smooth_group(obj_mesh_data, export_params_, i);
if (group != prev_group) {
buf.write_obj_smooth(group);
}
}
/* Write vertex group if different from previous. */
if (export_params_.export_vertex_groups) {
Vector<float> &local_weights = group_weights.local();
local_weights.resize(tot_deform_groups);
const int16_t prev_group = idx == 0 ? NEGATIVE_INIT :
obj_mesh_data.get_face_deform_group_index(
prev_i, local_weights);
const int16_t group = obj_mesh_data.get_face_deform_group_index(i, local_weights);
if (group != prev_group) {
buf.write_obj_group(group == NOT_FOUND ? DEFORM_GROUP_DISABLED :
obj_mesh_data.get_face_deform_group_name(group));
}
}
/* Write material name and material group if different from previous. */
if ((export_params_.export_materials || export_params_.export_material_groups) &&
obj_mesh_data.tot_materials() > 0)
{
const int16_t prev_mat = idx == 0 ? NEGATIVE_INIT : std::max(0, material_indices[prev_i]);
const int16_t mat = std::max(0, material_indices[i]);
if (mat != prev_mat) {
if (mat == NOT_FOUND) {
if (export_params_.export_materials) {
buf.write_obj_usemtl(MATERIAL_GROUP_DISABLED);
}
}
else {
const char *mat_name = matname_fn(mat);
if (!mat_name) {
mat_name = MATERIAL_GROUP_DISABLED;
}
if (export_params_.export_material_groups) {
std::string object_name = obj_mesh_data.get_object_name();
spaces_to_underscores(object_name);
buf.write_obj_group(object_name + "_" + mat_name);
}
if (export_params_.export_materials) {
buf.write_obj_usemtl(mat_name);
}
}
}
}
/* Write face elements. */
(this->*face_element_writer)(buf,
offsets,
face_vertex_indices,
face_uv_indices,
face_normal_indices,
obj_mesh_data.is_mirrored_transform());
});
}
void OBJWriter::write_edges_indices(FormatHandler &fh,
const IndexOffsets &offsets,
const OBJMesh &obj_mesh_data) const
{
const Mesh &mesh = *obj_mesh_data.get_mesh();
const Span<int2> edges = mesh.edges();
mesh.loose_edges().foreach_index([&](const int i) {
const int2 obj_edge = edges[i] + offsets.vertex_offset + 1;
fh.write_obj_edge(obj_edge[0], obj_edge[1]);
});
}
static float4x4 compute_world_axes_transform(const OBJExportParams &export_params,
const float4x4 &object_to_world)
{
float4x4 world_axes_transform;
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(
export_params.forward_axis, export_params.up_axis, IO_AXIS_Y, IO_AXIS_Z, axes_transform);
mul_m4_m3m4(world_axes_transform.ptr(), axes_transform, object_to_world.ptr());
/* #mul_m4_m3m4 does not transform last row of #Object.object_to_world, i.e. location data. */
mul_v3_m3v3(world_axes_transform[3], axes_transform, object_to_world.location());
world_axes_transform[3][3] = object_to_world[3][3];
/* Apply global scale transform. */
mul_v3_fl(world_axes_transform[0], export_params.global_scale);
mul_v3_fl(world_axes_transform[1], export_params.global_scale);
mul_v3_fl(world_axes_transform[2], export_params.global_scale);
mul_v3_fl(world_axes_transform[3], export_params.global_scale);
return world_axes_transform;
}
void OBJWriter::write_nurbs_curve(FormatHandler &fh, const IOBJCurve &obj_nurbs_data) const
{
const int total_splines = obj_nurbs_data.total_splines();
for (int spline_idx = 0; spline_idx < total_splines; spline_idx++) {
/* Double check no surface is passed in as they are no supported (this is filtered when parsed)
*/
BLI_assert(obj_nurbs_data.num_control_points_v(spline_idx) == 1);
const float4x4 world_axes_transform = compute_world_axes_transform(
export_params_, obj_nurbs_data.object_transform());
const char *nurbs_name = obj_nurbs_data.get_curve_name();
const int degree_u = obj_nurbs_data.get_nurbs_degree_u(spline_idx);
fh.write_obj_group(nurbs_name);
fh.write_obj_cstype();
fh.write_obj_nurbs_degree(degree_u);
const int num_points_u = obj_nurbs_data.num_control_points_u(spline_idx);
Vector<float> knot_buffer;
Span<float> knots_u = obj_nurbs_data.get_knots_u(spline_idx, knot_buffer);
IndexRange point_range(0, num_points_u);
knots_u = valid_nurb_control_point_range(degree_u + 1, knots_u, point_range);
/* Write coords */
Vector<float3> dynamic_point_buffer;
Span<float3> vertex_coords = obj_nurbs_data.vertex_coordinates(spline_idx,
dynamic_point_buffer);
/* Write only unique points. */
IndexRange point_loop_range = point_range.size() > vertex_coords.size() ?
point_range.drop_back(point_range.size() -
vertex_coords.size()) :
point_range;
for (const int64_t index : point_loop_range) {
/* Modulo will loop back to the 0:th point, not the start of the point range! */
float3 co = vertex_coords[index % vertex_coords.size()];
mul_m4_v3(world_axes_transform.ptr(), &co.x);
fh.write_obj_vertex(co[0], co[1], co[2]);
}
fh.write_obj_curve_begin();
fh.write_obj_nurbs_parm(knots_u[degree_u]);
fh.write_obj_nurbs_parm(knots_u.last(degree_u));
/* Loop over the [0, N) range, not its actual interval [x, N + x).
* For cyclic curves, up to [0, order) will be repeated.
*/
for (int64_t index : point_range.index_range()) {
/* Write one based (1 ==> coords[0]) relative/negative indices.
* TODO: Write positive indices...
*/
index = -(point_loop_range.size() - (index % point_loop_range.size()));
fh.write_obj_face_v(index);
}
fh.write_obj_curve_end();
/* Write knot vector. */
fh.write_obj_nurbs_parm_begin();
for (const float &u : knots_u) {
fh.write_obj_nurbs_parm(u);
}
fh.write_obj_nurbs_parm_end();
fh.write_obj_nurbs_group_end();
}
}
/* -------------------------------------------------------------------- */
/** \name `.MTL` writers.
* \{ */
static const char *tex_map_type_to_string[] = {
"map_Kd",
"map_Pm",
"map_Ks",
"map_Ns",
"map_Pr",
"map_Ps",
"map_refl",
"map_Ke",
"map_d",
"map_Bump",
};
BLI_STATIC_ASSERT(ARRAY_SIZE(tex_map_type_to_string) == int(MTLTexMapType::Count),
"array size mismatch");
/**
* 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)
{
return fmt::format("{} {} {}", numbers[0], numbers[1], numbers[2]);
}
MTLWriter::MTLWriter(const char *obj_filepath, bool write_file) noexcept(false)
{
if (!write_file) {
return;
}
char mtl_path[FILE_MAX];
STRNCPY(mtl_path, obj_filepath);
const bool ok = BLI_path_extension_replace(mtl_path, sizeof(mtl_path), ".mtl");
if (!ok) {
throw std::system_error(ENAMETOOLONG, std::system_category(), "");
}
mtl_filepath_ = mtl_path;
outfile_ = BLI_fopen(mtl_filepath_.c_str(), "wb");
if (!outfile_) {
throw std::system_error(errno, std::system_category(), "Cannot open file " + mtl_filepath_);
}
}
MTLWriter::~MTLWriter()
{
if (outfile_) {
fmt_handler_.write_to_file(outfile_);
if (std::fclose(outfile_)) {
CLOG_ERROR(&LOG,
"Error: could not close file '%s' properly, it may be corrupted.",
mtl_filepath_.c_str());
}
}
}
void MTLWriter::write_header(const char *blen_filepath)
{
using namespace std::string_literals;
const char *blen_basename = (blen_filepath && blen_filepath[0] != '\0') ?
BLI_path_basename(blen_filepath) :
"None";
fmt_handler_.write_string("# Blender "s + BKE_blender_version_string() + " MTL File: '" +
blen_basename + "'");
fmt_handler_.write_string("# www.blender.org");
}
StringRefNull MTLWriter::mtl_file_path() const
{
return mtl_filepath_;
}
void MTLWriter::write_bsdf_properties(const MTLMaterial &mtl, bool write_pbr)
{
/* For various material properties, we only capture information
* coming from the texture, or the default value of the socket.
* When the texture is present, do not emit the default value. */
/* Do not write Ns & Ka when writing in PBR mode. */
if (!write_pbr) {
if (!mtl.tex_map_of_type(MTLTexMapType::SpecularExponent).is_valid()) {
fmt_handler_.write_mtl_float("Ns", mtl.spec_exponent);
}
fmt_handler_.write_mtl_float3(
"Ka", mtl.ambient_color.x, mtl.ambient_color.y, mtl.ambient_color.z);
}
if (!mtl.tex_map_of_type(MTLTexMapType::Color).is_valid()) {
fmt_handler_.write_mtl_float3("Kd", mtl.color.x, mtl.color.y, mtl.color.z);
}
if (!mtl.tex_map_of_type(MTLTexMapType::Specular).is_valid()) {
fmt_handler_.write_mtl_float3("Ks", mtl.spec_color.x, mtl.spec_color.y, mtl.spec_color.z);
}
if (!mtl.tex_map_of_type(MTLTexMapType::Emission).is_valid()) {
fmt_handler_.write_mtl_float3(
"Ke", mtl.emission_color.x, mtl.emission_color.y, mtl.emission_color.z);
}
fmt_handler_.write_mtl_float("Ni", mtl.ior);
if (!mtl.tex_map_of_type(MTLTexMapType::Alpha).is_valid()) {
fmt_handler_.write_mtl_float("d", mtl.alpha);
}
fmt_handler_.write_mtl_illum(mtl.illum_mode);
if (write_pbr) {
if (!mtl.tex_map_of_type(MTLTexMapType::Roughness).is_valid() && mtl.roughness >= 0.0f) {
fmt_handler_.write_mtl_float("Pr", mtl.roughness);
}
if (!mtl.tex_map_of_type(MTLTexMapType::Metallic).is_valid() && mtl.metallic >= 0.0f) {
fmt_handler_.write_mtl_float("Pm", mtl.metallic);
}
if (!mtl.tex_map_of_type(MTLTexMapType::Sheen).is_valid() && mtl.sheen >= 0.0f) {
fmt_handler_.write_mtl_float("Ps", mtl.sheen);
}
if (mtl.cc_thickness >= 0.0f) {
fmt_handler_.write_mtl_float("Pc", mtl.cc_thickness);
}
if (mtl.cc_roughness >= 0.0f) {
fmt_handler_.write_mtl_float("Pcr", mtl.cc_roughness);
}
if (mtl.aniso >= 0.0f) {
fmt_handler_.write_mtl_float("aniso", mtl.aniso);
}
if (mtl.aniso_rot >= 0.0f) {
fmt_handler_.write_mtl_float("anisor", mtl.aniso_rot);
}
if (mtl.transmit_color.x > 0.0f || mtl.transmit_color.y > 0.0f || mtl.transmit_color.z > 0.0f)
{
fmt_handler_.write_mtl_float3(
"Tf", mtl.transmit_color.x, mtl.transmit_color.y, mtl.transmit_color.z);
}
}
}
void MTLWriter::write_texture_map(const MTLMaterial &mtl_material,
MTLTexMapType texture_key,
const MTLTexMap &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. */
if (texture_map.translation != float3{0.0f, 0.0f, 0.0f}) {
options.append(" -o ").append(float3_to_string(texture_map.translation));
}
if (texture_map.scale != float3{1.0f, 1.0f, 1.0f}) {
options.append(" -s ").append(float3_to_string(texture_map.scale));
}
if (texture_key == MTLTexMapType::Normal && mtl_material.normal_strength > 0.0001f) {
options.append(" -bm ").append(std::to_string(mtl_material.normal_strength));
}
std::string path = path_reference(
texture_map.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_mtl_map(tex_map_type_to_string[int(texture_key)], options, path);
}
static bool is_pbr_map(MTLTexMapType type)
{
return type == MTLTexMapType::Metallic || type == MTLTexMapType::Roughness ||
type == MTLTexMapType::Sheen;
}
static bool is_non_pbr_map(MTLTexMapType type)
{
return type == MTLTexMapType::SpecularExponent || type == MTLTexMapType::Reflection;
}
void MTLWriter::write_materials(const char *blen_filepath,
ePathReferenceMode path_mode,
const char *dest_dir,
bool write_pbr)
{
if (mtlmaterials_.is_empty()) {
return;
}
char blen_filedir[FILE_MAX];
BLI_path_split_dir_part(blen_filepath, blen_filedir, sizeof(blen_filedir));
BLI_path_slash_native(blen_filedir);
BLI_path_normalize(blen_filedir);
std::ranges::sort(mtlmaterials_,
[](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_string("");
fmt_handler_.write_mtl_newmtl(mtlmat.name);
write_bsdf_properties(mtlmat, write_pbr);
for (int key = 0; key < int(MTLTexMapType::Count); key++) {
const MTLTexMap &tex = mtlmat.texture_maps[key];
if (!tex.is_valid()) {
continue;
}
if (!write_pbr && is_pbr_map((MTLTexMapType)key)) {
continue;
}
if (write_pbr && is_non_pbr_map((MTLTexMapType)key)) {
continue;
}
write_texture_map(
mtlmat, (MTLTexMapType)key, tex, blen_filedir, dest_dir, path_mode, copy_set);
}
}
path_reference_copy(copy_set);
}
Vector<int> MTLWriter::add_materials(const OBJMesh &mesh_to_export)
{
Vector<int> mtl_indices;
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.materials[i];
if (!material) {
mtl_indices[i] = -1;
continue;
}
int mtlmat_index = material_map_.lookup_default(material, -1);
if (mtlmat_index != -1) {
mtl_indices[i] = mtlmat_index;
}
else {
mtlmaterials_.append(mtlmaterial_for_material(material));
mtl_indices[i] = mtlmaterials_.size() - 1;
material_map_.add_new(material, mtl_indices[i]);
}
}
return mtl_indices;
}
const char *MTLWriter::mtlmaterial_name(int index)
{
if (index < 0 || index >= mtlmaterials_.size()) {
return nullptr;
}
return mtlmaterials_[index].name.c_str();
}
/** \} */
} // namespace io::obj
} // namespace blender

View File

@@ -0,0 +1,207 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "BLI_map.hh"
#include "BLI_set.hh"
#include "BLI_vector.hh"
#include "IO_wavefront_obj.hh"
#include "obj_export_io.hh"
#include "obj_export_mesh.hh"
#include "obj_export_mtl.hh"
namespace blender::io::obj {
class IOBJCurve;
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::string outfile_path_;
FILE *outfile_;
public:
OBJWriter(const char *filepath, const OBJExportParams &export_params) noexcept(false);
~OBJWriter();
FILE *get_outfile() const
{
return outfile_;
}
void write_header() const;
/**
* Write object's name or group.
*/
void write_object_name(FormatHandler &fh, const OBJMesh &obj_mesh_data) const;
/**
* Write file name of Material Library in `.OBJ` file.
*/
void write_mtllib_name(StringRefNull mtl_filepath) const;
/**
* Write vertex coordinates for all vertices as "v x y z" or "v x y z r g b".
*/
void write_vertex_coords(FormatHandler &fh,
const OBJMesh &obj_mesh_data,
bool write_colors) const;
/**
* Write UV vertex coordinates for all vertices as `vt u v`.
* \note UV indices are stored here, but written with faces later.
*/
void write_uv_coords(FormatHandler &fh, OBJMesh &obj_mesh_data) const;
/**
* Write corner normals for smooth-shaded faces, and face normals otherwise, as "vn x y z".
* \note Normal indices ares stored here, but written with faces later.
*/
void write_normals(FormatHandler &fh, OBJMesh &obj_mesh_data);
/**
* Write face elements with at least vertex indices, and conditionally with UV vertex
* indices and face 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 write_face_elements(FormatHandler &fh,
const IndexOffsets &offsets,
const OBJMesh &obj_mesh_data,
FunctionRef<const char *(int)> matname_fn);
/**
* Write loose edges of a mesh as "l v1 v2".
*/
void write_edges_indices(FormatHandler &fh,
const IndexOffsets &offsets,
const OBJMesh &obj_mesh_data) const;
/**
* Write a NURBS curve to the `.OBJ` file in parameter form.
*/
void write_nurbs_curve(FormatHandler &fh, const IOBJCurve &obj_nurbs_data) const;
private:
using func_vert_uv_normal_indices = void (OBJWriter::*)(FormatHandler &fh,
const IndexOffsets &offsets,
Span<int> vert_indices,
Span<int> uv_indices,
Span<int> normal_indices,
bool flip) const;
/**
* \return Writer function with appropriate face-element syntax.
*/
func_vert_uv_normal_indices get_face_element_writer(int total_uv_vertices) const;
/**
* Write one line of face indices as "f v1/vt1/vn1 v2/vt2/vn2 ...".
*/
void write_vert_uv_normal_indices(FormatHandler &fh,
const IndexOffsets &offsets,
Span<int> vert_indices,
Span<int> uv_indices,
Span<int> normal_indices,
bool flip) const;
/**
* Write one line of face indices as "f v1//vn1 v2//vn2 ...".
*/
void write_vert_normal_indices(FormatHandler &fh,
const IndexOffsets &offsets,
Span<int> vert_indices,
Span<int> /*uv_indices*/,
Span<int> normal_indices,
bool flip) const;
/**
* Write one line of face indices as "f v1/vt1 v2/vt2 ...".
*/
void write_vert_uv_indices(FormatHandler &fh,
const IndexOffsets &offsets,
Span<int> vert_indices,
Span<int> uv_indices,
Span<int> /*normal_indices*/,
bool flip) const;
/**
* Write one line of face indices as "f v1 v2 ...".
*/
void write_vert_indices(FormatHandler &fh,
const IndexOffsets &offsets,
Span<int> vert_indices,
Span<int> /*uv_indices*/,
Span<int> /*normal_indices*/,
bool flip) const;
};
/**
* Responsible for writing a `.MTL` file.
*/
class MTLWriter : NonMovable, NonCopyable {
private:
FormatHandler fmt_handler_;
FILE *outfile_ = nullptr;
std::string mtl_filepath_;
Vector<MTLMaterial> mtlmaterials_;
/* Map from a Material* to an index into mtlmaterials_. */
Map<const Material *, int> material_map_;
public:
/*
* Create the `.MTL` file.
*/
MTLWriter(const char *obj_filepath, bool write_file) noexcept(false);
~MTLWriter();
void write_header(const char *blen_filepath);
/**
* 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 write_materials(const char *blen_filepath,
ePathReferenceMode path_mode,
const char *dest_dir,
bool write_pbr);
StringRefNull mtl_file_path() const;
/**
* Add the materials of the given object to #MTLWriter, de-duplicating
* 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> add_materials(const OBJMesh &mesh_to_export);
const char *mtlmaterial_name(int index);
private:
/**
* Write properties sourced from p-BSDF node or #Object.Material.
*/
void write_bsdf_properties(const MTLMaterial &mtl_material, bool write_pbr);
/**
* 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,
MTLTexMapType texture_key,
const MTLTexMap &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

@@ -0,0 +1,218 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include <cstdio>
#include "BLI_string_ref.hh"
#include "BLI_utility_mixins.hh"
#include "BLI_vector.hh"
#include <fmt/format.h>
namespace blender::io::obj {
/**
* File buffer writer.
* All writes are done into an internal chunked memory buffer
* (list of default 64 kilobyte blocks).
* Call write_fo_file once in a while to write the memory buffer(s)
* into the given file.
*/
class FormatHandler : NonCopyable, NonMovable {
private:
using VectorChar = Vector<char>;
Vector<VectorChar> blocks_;
size_t buffer_chunk_size_;
public:
FormatHandler(size_t buffer_chunk_size = 64 * 1024) : buffer_chunk_size_(buffer_chunk_size) {}
/* Write contents to the buffer(s) into a file, and clear the buffers. */
void write_to_file(FILE *f)
{
for (const auto &b : blocks_) {
fwrite(b.data(), 1, b.size(), f);
}
blocks_.clear();
}
std::string get_as_string() const
{
std::string s;
for (const auto &b : blocks_) {
s.append(b.data(), b.size());
}
return s;
}
size_t get_block_count() const
{
return blocks_.size();
}
void append_from(FormatHandler &v)
{
blocks_.insert(blocks_.end(),
std::make_move_iterator(v.blocks_.begin()),
std::make_move_iterator(v.blocks_.end()));
v.blocks_.clear();
}
void write_obj_vertex(float x, float y, float z)
{
write_impl("v {:.6f} {:.6f} {:.6f}\n", x, y, z);
}
void write_obj_vertex_color(float x, float y, float z, float r, float g, float b)
{
write_impl("v {:.6f} {:.6f} {:.6f} {:.4f} {:.4f} {:.4f}\n", x, y, z, r, g, b);
}
void write_obj_uv(float x, float y)
{
write_impl("vt {:.6f} {:.6f}\n", x, y);
}
void write_obj_normal(float x, float y, float z)
{
write_impl("vn {:.4f} {:.4f} {:.4f}\n", x, y, z);
}
void write_obj_face_begin()
{
write_impl("f");
}
void write_obj_face_end()
{
write_obj_newline();
}
void write_obj_face_v_uv_normal(int v, int uv, int n)
{
write_impl(" {}/{}/{}", v, uv, n);
}
void write_obj_face_v_normal(int v, int n)
{
write_impl(" {}//{}", v, n);
}
void write_obj_face_v_uv(int v, int uv)
{
write_impl(" {}/{}", v, uv);
}
void write_obj_face_v(int v)
{
write_impl(" {}", v);
}
void write_obj_usemtl(StringRef s)
{
write_impl("usemtl {}\n", s);
}
void write_obj_mtllib(StringRef s)
{
write_impl("mtllib {}\n", s);
}
void write_obj_smooth(int s)
{
write_impl("s {}\n", s);
}
void write_obj_group(StringRef s)
{
write_impl("g {}\n", s);
}
void write_obj_object(StringRef s)
{
write_impl("o {}\n", s);
}
void write_obj_edge(int a, int b)
{
write_impl("l {} {}\n", a, b);
}
void write_obj_cstype()
{
write_impl("cstype bspline\n");
}
void write_obj_nurbs_degree(int deg)
{
write_impl("deg {}\n", deg);
}
void write_obj_curve_begin()
{
write_impl("curv");
}
void write_obj_curve_end()
{
write_obj_newline();
}
void write_obj_nurbs_parm_begin()
{
write_impl("parm u");
}
void write_obj_nurbs_parm(float v)
{
write_impl(" {:.6f}", v);
}
void write_obj_nurbs_parm_end()
{
write_impl("\n");
}
void write_obj_nurbs_group_end()
{
write_impl("end\n");
}
void write_obj_newline()
{
write_impl("\n");
}
void write_mtl_newmtl(StringRef s)
{
write_impl("newmtl {}\n", s);
}
void write_mtl_float(const char *type, float v)
{
write_impl("{} {:.6f}\n", type, v);
}
void write_mtl_float3(const char *type, float r, float g, float b)
{
write_impl("{} {:.6f} {:.6f} {:.6f}\n", type, r, g, b);
}
void write_mtl_illum(int mode)
{
write_impl("illum {}\n", mode);
}
/* NOTE: options, if present, will have its own leading space. */
void write_mtl_map(const char *type, StringRef options, StringRef value)
{
write_impl("{}{} {}\n", type, options, value);
}
void write_string(StringRef s)
{
write_impl("{}\n", s);
}
private:
/* Ensure the last block contains at least this amount of free space.
* If not, add a new block with max of block size & the amount of space needed. */
void ensure_space(size_t at_least)
{
if (blocks_.is_empty() || (blocks_.last().capacity() - blocks_.last().size() < at_least)) {
blocks_.append(VectorChar());
blocks_.last().reserve(std::max(at_least, buffer_chunk_size_));
}
}
template<typename... T> void write_impl(fmt::format_string<T...> fmt, T &&...args)
{
/* Format into a local buffer. */
fmt::memory_buffer buf;
fmt::format_to(fmt::appender(buf), fmt, std::forward<T>(args)...);
size_t len = buf.size();
ensure_space(len);
VectorChar &bb = blocks_.last();
bb.insert(bb.end(), buf.begin(), buf.end());
}
};
} // namespace blender::io::obj

View File

@@ -0,0 +1,422 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include "BKE_attribute.hh"
#include "BKE_customdata.hh"
#include "BKE_deform.hh"
#include "BKE_lib_id.hh"
#include "BKE_material.hh"
#include "BKE_mesh.hh"
#include "BKE_mesh_mapping.hh"
#include "BKE_object.hh"
#include "BLI_array_utils.hh"
#include "BLI_listbase.h"
#include "BLI_map.hh"
#include "BLI_math_matrix.hh"
#include "BLI_math_rotation.h"
#include "BLI_sort.hh"
#include "BLI_vector_set.hh"
#include "DEG_depsgraph_query.hh"
#include "DNA_meshdata_types.h"
#include "DNA_modifier_types.h"
#include "DNA_object_types.h"
#include "obj_export_mesh.hh"
#include "bmesh.hh"
#include "bmesh_tools.hh"
namespace blender::io::obj {
OBJMesh::OBJMesh(Depsgraph *depsgraph, const OBJExportParams &export_params, Object *mesh_object)
{
/* We need to copy the object because it may be in temporary space. */
Object *obj_eval = DEG_get_evaluated(depsgraph, mesh_object);
object_name_ = obj_eval->id.name + 2;
export_mesh_ = nullptr;
if (obj_eval->type == OB_MESH) {
export_mesh_ = export_params.apply_modifiers ? BKE_object_get_evaluated_mesh(obj_eval) :
BKE_object_get_pre_modified_mesh(obj_eval);
}
if (export_mesh_) {
mesh_edges_ = export_mesh_->edges();
mesh_faces_ = export_mesh_->faces();
mesh_corner_verts_ = export_mesh_->corner_verts();
sharp_faces_ = *export_mesh_->attributes().lookup_or_default<bool>(
"sharp_face", bke::AttrDomain::Face, false);
}
else {
/* Curves and NURBS surfaces need a new mesh when they're
* exported in the form of vertices and edges.
*/
this->set_mesh(BKE_mesh_new_from_object(depsgraph, obj_eval, true, true, true));
}
if (export_params.export_triangulated_mesh && obj_eval->type == OB_MESH) {
this->triangulate_mesh_eval();
}
this->materials.reinitialize(export_mesh_->totcol);
for (const int i : this->materials.index_range()) {
this->materials[i] = BKE_object_material_get_eval(obj_eval, i + 1);
}
set_world_axes_transform(*obj_eval,
export_params.forward_axis,
export_params.up_axis,
export_params.global_scale,
export_params.apply_transform);
}
/**
* Free new meshes allocated for triangulated meshes, or Curve converted to Mesh.
*/
OBJMesh::~OBJMesh()
{
clear();
}
void OBJMesh::set_mesh(Mesh *mesh)
{
if (owned_export_mesh_) {
BKE_id_free(nullptr, owned_export_mesh_);
}
owned_export_mesh_ = mesh;
export_mesh_ = owned_export_mesh_;
mesh_edges_ = mesh->edges();
mesh_faces_ = mesh->faces();
mesh_corner_verts_ = mesh->corner_verts();
sharp_faces_ = *export_mesh_->attributes().lookup_or_default<bool>(
"sharp_face", bke::AttrDomain::Face, false);
}
void OBJMesh::clear()
{
if (owned_export_mesh_) {
BKE_id_free(nullptr, owned_export_mesh_);
owned_export_mesh_ = nullptr;
}
export_mesh_ = nullptr;
corner_to_uv_index_ = {};
uv_coords_.clear_and_shrink();
corner_to_normal_index_ = {};
normal_coords_ = {};
face_order_ = {};
if (face_smooth_groups_) {
MEM_delete(face_smooth_groups_);
face_smooth_groups_ = nullptr;
}
}
void OBJMesh::triangulate_mesh_eval()
{
if (export_mesh_->faces_num <= 0) {
return;
}
const BMeshCreateParams bm_create_params = {false};
BMeshFromMeshParams bm_convert_params{};
bm_convert_params.calc_face_normal = true;
bm_convert_params.calc_vert_normal = true;
bm_convert_params.add_key_index = false;
bm_convert_params.use_shapekey = false;
/* Lower threshold where triangulation of a face starts, i.e. a quadrilateral will be
* triangulated here. */
const int triangulate_min_verts = 4;
BMesh *bmesh = BKE_mesh_to_bmesh_ex(export_mesh_, &bm_create_params, &bm_convert_params);
BM_mesh_triangulate(bmesh,
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, nullptr, export_mesh_);
BM_mesh_free(bmesh);
this->set_mesh(triangulated);
}
void OBJMesh::set_world_axes_transform(const Object &obj_eval,
const eIOAxis forward,
const eIOAxis up,
const float global_scale,
const bool apply_transform)
{
float3x3 axes_transform;
/* +Y-forward and +Z-up are the default Blender axis settings. */
mat3_from_axis_conversion(forward, up, IO_AXIS_Y, IO_AXIS_Z, axes_transform.ptr());
const float4x4 &object_to_world = apply_transform ? obj_eval.object_to_world() :
float4x4::identity();
const float3x3 transform = axes_transform * float3x3(object_to_world);
world_and_axes_transform_ = float4x4(transform);
world_and_axes_transform_.location() = axes_transform * object_to_world.location();
world_and_axes_transform_[3][3] = object_to_world[3][3];
world_and_axes_transform_ = math::from_scale<float4x4>(float3(global_scale)) *
world_and_axes_transform_;
/* Normals need inverse transpose of the regular matrix to handle non-uniform scale. */
world_and_axes_normal_transform_ = math::transpose(math::invert(transform));
mirrored_transform_ = math::is_negative(world_and_axes_normal_transform_);
}
int OBJMesh::tot_vertices() const
{
return export_mesh_->verts_num;
}
int OBJMesh::tot_faces() const
{
return export_mesh_->faces_num;
}
int OBJMesh::tot_uv_vertices() const
{
return int(uv_coords_.size());
}
int OBJMesh::tot_edges() const
{
return export_mesh_->edges_num;
}
int16_t OBJMesh::tot_materials() const
{
return this->materials.size();
}
int OBJMesh::ith_smooth_group(const int face_index) const
{
/* Calculate smooth groups first: #OBJMesh::calc_smooth_groups. */
BLI_assert(tot_smooth_groups_ != -NEGATIVE_INIT);
BLI_assert(face_smooth_groups_);
return face_smooth_groups_[face_index];
}
void OBJMesh::calc_smooth_groups(const bool use_bitflags)
{
const bke::AttributeAccessor attributes = export_mesh_->attributes();
const VArraySpan sharp_edges = *attributes.lookup<bool>("sharp_edge", bke::AttrDomain::Edge);
const VArraySpan sharp_faces = *attributes.lookup<bool>("sharp_face", bke::AttrDomain::Face);
if (use_bitflags) {
face_smooth_groups_ = BKE_mesh_calc_smoothgroups_bitflags(mesh_edges_.size(),
export_mesh_->verts_num,
mesh_faces_,
export_mesh_->corner_edges(),
export_mesh_->corner_verts(),
sharp_edges,
sharp_faces,
true,
&tot_smooth_groups_);
}
else {
face_smooth_groups_ = BKE_mesh_calc_smoothgroups(mesh_edges_.size(),
mesh_faces_,
export_mesh_->corner_edges(),
sharp_edges,
sharp_faces,
&tot_smooth_groups_);
}
}
void OBJMesh::calc_face_order()
{
const bke::AttributeAccessor attributes = export_mesh_->attributes();
const VArray<int> material_indices = *attributes.lookup_or_default<int>(
"material_index", bke::AttrDomain::Face, 0);
if (material_indices.is_single() && material_indices.get_internal_single() == 0) {
return;
}
const VArraySpan<int> material_indices_span(material_indices);
/* Sort faces by their material index. */
face_order_.reinitialize(material_indices_span.size());
array_utils::fill_index_range(face_order_.as_mutable_span());
parallel_sort(face_order_.begin(), face_order_.end(), [&](int a, int b) {
int mat_a = material_indices_span[a];
int mat_b = material_indices_span[b];
if (mat_a != mat_b) {
return mat_a < mat_b;
}
return a < b;
});
}
bool OBJMesh::is_ith_face_smooth(const int face_index) const
{
return !sharp_faces_[face_index];
}
StringRef OBJMesh::get_object_name() const
{
return object_name_;
}
StringRef OBJMesh::get_object_mesh_name() const
{
return export_mesh_->id.name + 2;
}
void OBJMesh::store_uv_coords_and_indices()
{
const StringRef active_uv_name = export_mesh_->active_uv_map_name();
if (active_uv_name.is_empty()) {
uv_coords_.clear();
return;
}
const bke::AttributeAccessor attributes = export_mesh_->attributes();
const VArraySpan uv_map = *attributes.lookup<float2>(active_uv_name, bke::AttrDomain::Corner);
if (uv_map.is_empty()) {
uv_coords_.clear();
return;
}
Map<float2, int> uv_to_index;
/* We don't know how many unique UVs there will be, but this is a guess. */
uv_to_index.reserve(export_mesh_->verts_num);
uv_coords_.reserve(export_mesh_->verts_num);
corner_to_uv_index_.reinitialize(uv_map.size());
for (int index = 0; index < int(uv_map.size()); index++) {
float2 uv = uv_map[index];
int uv_index = uv_to_index.lookup_default(uv, -1);
if (uv_index == -1) {
uv_index = uv_to_index.size();
uv_to_index.add(uv, uv_index);
uv_coords_.append(uv);
}
corner_to_uv_index_[index] = uv_index;
}
}
/** Round \a f to \a round_digits decimal digits. */
static float round_float_to_n_digits(const float f, int round_digits)
{
float scale = powf(10.0, round_digits);
return ceilf(scale * f - 0.49999999f) / scale;
}
static float3 round_float3_to_n_digits(const float3 &v, int round_digits)
{
float3 ans;
ans.x = round_float_to_n_digits(v.x, round_digits);
ans.y = round_float_to_n_digits(v.y, round_digits);
ans.z = round_float_to_n_digits(v.z, round_digits);
return ans;
}
void OBJMesh::store_normal_coords_and_indices()
{
/* We'll round normal components to 4 digits.
* This will cover up some minor differences
* between floating point calculations on different platforms.
* Since normals are normalized, there will be no perceptible loss
* of precision when rounding to 4 digits. */
constexpr int round_digits = 4;
VectorSet<float3> unique_normals;
/* We don't know how many unique normals there will be, but this is a guess. */
unique_normals.reserve(export_mesh_->faces_num);
corner_to_normal_index_.reinitialize(export_mesh_->corners_num);
/* Normals need inverse transpose of the regular matrix to handle non-uniform scale. */
const float3x3 transform = world_and_axes_normal_transform_;
auto add_normal = [&](const float3 &normal) {
const float3 transformed = math::normalize(transform * normal);
const float3 rounded = round_float3_to_n_digits(transformed, round_digits);
return unique_normals.index_of_or_add(rounded);
};
switch (export_mesh_->normals_domain()) {
case bke::MeshNormalDomain::Face: {
const Span<float3> face_normals = export_mesh_->face_normals();
for (const int face : mesh_faces_.index_range()) {
const int index = add_normal(face_normals[face]);
corner_to_normal_index_.as_mutable_span().slice(mesh_faces_[face]).fill(index);
}
break;
}
case bke::MeshNormalDomain::Point: {
const Span<float3> vert_normals = export_mesh_->vert_normals();
Array<int> vert_normal_indices(vert_normals.size());
const IndexMask &verts_no_face = export_mesh_->verts_no_face();
IndexMaskMemory memory;
const IndexMask verts = verts_no_face.complement(vert_normals.index_range(), memory);
verts.foreach_index(
[&](const int vert) { vert_normal_indices[vert] = add_normal(vert_normals[vert]); });
array_utils::gather(vert_normal_indices.as_span(),
mesh_corner_verts_,
corner_to_normal_index_.as_mutable_span());
break;
}
case bke::MeshNormalDomain::Corner: {
const Span<float3> corner_normals = export_mesh_->corner_normals();
for (const int corner : corner_normals.index_range()) {
corner_to_normal_index_[corner] = add_normal(corner_normals[corner]);
}
break;
}
}
normal_coords_ = unique_normals.as_span();
}
int OBJMesh::tot_deform_groups() const
{
return export_mesh_->vertex_group_names.count();
}
int16_t OBJMesh::get_face_deform_group_index(const int face_index,
MutableSpan<float> group_weights) const
{
BLI_assert(face_index < export_mesh_->faces_num);
BLI_assert(group_weights.size() == export_mesh_->vertex_group_names.count());
const Span<MDeformVert> dverts = export_mesh_->deform_verts();
if (dverts.is_empty()) {
return NOT_FOUND;
}
group_weights.fill(0);
bool found_any_group = false;
for (const int vert : mesh_corner_verts_.slice(mesh_faces_[face_index])) {
const MDeformVert &dv = dverts[vert];
for (int weight_i = 0; weight_i < dv.totweight; ++weight_i) {
const auto group = dv.dw[weight_i].def_nr;
if (group < group_weights.size()) {
group_weights[group] += dv.dw[weight_i].weight;
found_any_group = true;
}
}
}
if (!found_any_group) {
return NOT_FOUND;
}
/* Index of the group with maximum vertices. */
int16_t max_idx = std::max_element(group_weights.begin(), group_weights.end()) -
group_weights.begin();
return max_idx;
}
const char *OBJMesh::get_face_deform_group_name(const int16_t def_group_index) const
{
const bDeformGroup &vertex_group = *(static_cast<bDeformGroup *>(
BLI_findlink(&export_mesh_->vertex_group_names, def_group_index)));
return vertex_group.name;
}
} // namespace blender::io::obj

View File

@@ -0,0 +1,229 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "BLI_math_matrix_types.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_offset_indices.hh"
#include "BLI_utility_mixins.hh"
#include "BLI_vector.hh"
#include "BLI_virtual_array.hh"
#include "DNA_material_types.h"
#include "DNA_object_types.h"
#include "IO_wavefront_obj.hh"
namespace blender::io::obj {
/** Denote absence for usually non-negative numbers. */
const int NOT_FOUND = -1;
/** Any negative number other than `NOT_FOUND` to initialize usually non-negative numbers. */
const int NEGATIVE_INIT = -10;
class OBJMesh : NonCopyable {
private:
std::string object_name_;
/** A pointer to #owned_export_mesh_ or the object'ed evaluated/original mesh. */
const Mesh *export_mesh_;
/** A mesh owned here, if created or modified for the export. May be null. */
Mesh *owned_export_mesh_ = nullptr;
Span<int2> mesh_edges_;
OffsetIndices<int> mesh_faces_;
Span<int> mesh_corner_verts_;
VArray<bool> sharp_faces_;
/**
* Final transform of an object obtained from export settings (up_axis, forward_axis) and the
* object's world transform matrix.
*/
float4x4 world_and_axes_transform_;
float3x3 world_and_axes_normal_transform_;
bool mirrored_transform_;
/** Per-corner UV index. */
Array<int> corner_to_uv_index_;
/** UV vertices. */
Vector<float2> uv_coords_;
/** Index into #normal_coords_ for every face corner. */
Array<int> corner_to_normal_index_;
/** De-duplicated normals, indexed by #corner_to_normal_index_. */
Array<float3> normal_coords_;
/**
* Total smooth groups in an object.
*/
int tot_smooth_groups_ = NEGATIVE_INIT;
/**
* Polygon aligned array of their smooth groups.
*/
int *face_smooth_groups_ = nullptr;
/**
* Order in which the faces should be written into the file (sorted by material index).
*/
Array<int> face_order_;
public:
Array<const Material *> materials;
/**
* Store evaluated Object and Mesh pointers. Conditionally triangulate a mesh, or
* create a new Mesh from a Curve.
*/
OBJMesh(Depsgraph *depsgraph, const OBJExportParams &export_params, Object *mesh_object);
~OBJMesh();
/* Clear various arrays to release potentially large memory allocations. */
void clear();
int tot_vertices() const;
int tot_faces() const;
int tot_uv_vertices() const;
int tot_edges() const;
int tot_deform_groups() const;
bool is_mirrored_transform() const
{
return mirrored_transform_;
}
/**
* \return Total materials in the object.
*/
int16_t tot_materials() const;
/**
* Calculate smooth groups of a smooth-shaded object.
* \return A face aligned array of smooth group numbers.
*/
void calc_smooth_groups(bool use_bitflags);
/**
* \return Smooth group of the face at the given index.
*/
int ith_smooth_group(int face_index) const;
bool is_ith_face_smooth(int face_index) const;
/**
* Get object name as it appears in the outliner.
*/
StringRef get_object_name() const;
/**
* Get Object's Mesh's name.
*/
StringRef get_object_mesh_name() const;
const float4x4 &get_world_axes_transform() const
{
return world_and_axes_transform_;
}
/**
* Calculate vertex indices of all vertices of the face at the given index.
*/
Span<int> calc_face_vert_indices(const int face_index) const
{
return mesh_corner_verts_.slice(mesh_faces_[face_index]);
}
/**
* Calculate UV vertex coordinates of an Object.
* Stores the coordinates and UV vertex indices in the member variables.
*/
void store_uv_coords_and_indices();
/* Get UV coordinates computed by store_uv_coords_and_indices. */
Span<float2> get_uv_coords() const
{
return uv_coords_;
}
Span<int> get_face_uv_indices(const int face_index) const
{
if (uv_coords_.is_empty()) {
return {};
}
BLI_assert(face_index < mesh_faces_.size());
return corner_to_uv_index_.as_span().slice(mesh_faces_[face_index]);
}
/**
* Find the unique normals of the mesh and stores them in a member variable.
* Also stores the indices into that vector with for each corner.
*/
void store_normal_coords_and_indices();
/* Get normals calculate by store_normal_coords_and_indices. */
Span<float3> get_normal_coords() const
{
return normal_coords_;
}
/**
* Calculate a face's face/corner normal indices.
* \param face_index: Index of the face to calculate indices for.
* \return Span of normal indices, aligned with vertices of face.
*/
Span<int> get_face_normal_indices(const int face_index) const
{
if (corner_to_normal_index_.is_empty()) {
return {};
}
const IndexRange face = mesh_faces_[face_index];
return corner_to_normal_index_.as_span().slice(face);
}
/**
* Find the most representative vertex group of a face.
*
* This adds up vertex group weights, and the group with the largest
* weight sum across the face is the one returned.
*
* group_weights is temporary storage to avoid reallocations, it must
* be the size of amount of vertex groups in the object.
*/
int16_t get_face_deform_group_index(int face_index, MutableSpan<float> group_weights) const;
/**
* Find the name of the vertex deform group at the given index.
* The index indices into the #Object.defbase.
*/
const char *get_face_deform_group_name(int16_t def_group_index) const;
/**
* Calculate the order in which the faces should be written into the file (sorted by material
* index).
*/
void calc_face_order();
/**
* Remap face index according to face writing order.
* When materials are not being written, the face order array
* might be empty, in which case remap is a no-op.
*/
int remap_face_index(int i) const
{
return i < 0 || i >= face_order_.size() ? i : face_order_[i];
}
const Mesh *get_mesh() const
{
return export_mesh_;
}
private:
/** Override the mesh from the export scene's object. Takes ownership of the mesh. */
void set_mesh(Mesh *mesh);
/**
* Triangulate the mesh pointed to by this object, potentially replacing it with a newly created
* mesh.
*/
void triangulate_mesh_eval();
/**
* Set the final transform after applying axes settings and an Object's world transform.
*/
void set_world_axes_transform(const Object &obj_eval,
eIOAxis forward,
eIOAxis up,
float global_scale,
bool apply_transform);
};
} // namespace blender::io::obj

View File

@@ -0,0 +1,399 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include "BKE_image.hh"
#include "BKE_node.hh"
#include "BKE_node_legacy_types.hh"
#include "BKE_node_runtime.hh"
#include "BLI_math_vector.h"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "DNA_material_types.h"
#include "DNA_node_types.h"
#include "obj_export_mtl.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.obj"};
namespace io::obj {
const char *tex_map_type_to_socket_id[] = {
"Base Color",
"Metallic",
"Specular IOR Level",
"Roughness", /* Map specular exponent to roughness. */
"Roughness",
"Sheen Weight",
"Metallic", /* Map reflection to metallic. */
"Emission Color",
"Alpha",
"Normal",
};
BLI_STATIC_ASSERT(ARRAY_SIZE(tex_map_type_to_socket_id) == int(MTLTexMapType::Count),
"array size mismatch");
/**
* 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;
}
const bNodeSocket *socket = bke::node_find_socket(
*const_cast<bNode *>(node), SOCK_IN, UString(identifier));
BLI_assert(socket && socket->type == property_type);
if (!socket) {
return;
}
switch (property_type) {
case SOCK_FLOAT: {
BLI_assert(r_property.size() == 1);
const bNodeSocketValueFloat *socket_def_value = static_cast<const bNodeSocketValueFloat *>(
socket->default_value);
r_property[0] = socket_def_value->value;
break;
}
case SOCK_RGBA: {
BLI_assert(r_property.size() == 3);
const bNodeSocketValueRGBA *socket_def_value = static_cast<const bNodeSocketValueRGBA *>(
socket->default_value);
copy_v3_v3(r_property.data(), socket_def_value->value);
break;
}
case SOCK_VECTOR: {
BLI_assert(r_property.size() == 3);
const bNodeSocketValueVector *socket_def_value = static_cast<const 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 bNodeTree &node_tree,
const char *dest_socket_id,
Vector<const bNodeSocket *> &r_linked_sockets)
{
r_linked_sockets.clear();
if (!dest_node) {
return;
}
Span<const bNode *> object_dest_nodes = node_tree.nodes_by_type(UString(dest_node->idname));
Span<const bNodeSocket *> dest_inputs = object_dest_nodes.first()->input_sockets();
const bNodeSocket *dest_socket = nullptr;
for (const bNodeSocket *curr_socket : dest_inputs) {
if (STREQ(curr_socket->identifier, dest_socket_id)) {
dest_socket = curr_socket;
break;
}
}
if (dest_socket) {
Span<const bNodeSocket *> 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 bNodeSocket *> sockets_list, const int node_type)
{
for (const bNodeSocket *socket : sockets_list) {
const bNode &parent_node = socket->owner_node();
if (parent_node.typeinfo->type_legacy == node_type) {
return &parent_node;
}
}
return nullptr;
}
/*
* From a texture image shader node, get the image's filepath.
* If packed image is found, only the file "name" is returned.
*/
static std::string get_image_filepath(const bNode *tex_node)
{
if (!tex_node) {
return "";
}
Image *tex_image = reinterpret_cast<Image *>(tex_node->id);
if (!tex_image || !BKE_image_has_filepath(tex_image)) {
return "";
}
if (BKE_image_has_packedfile(tex_image)) {
/* Put image in the same directory as the `.MTL` file. */
const char *filename = BLI_path_basename(tex_image->filepath);
CLOG_INFO(&LOG,
"Packed image found:'%s'. Unpack and place the image in the same "
"directory as the .MTL file.",
filename);
return filename;
}
char filepath[FILE_MAX];
STRNCPY(filepath, tex_image->filepath);
if (tex_image->source == IMA_SRC_SEQUENCE) {
char head[FILE_MAX], tail[FILE_MAX];
ushort numlen;
int framenr = static_cast<NodeTexImage *>(tex_node->storage)->iuser.framenr;
BLI_path_sequence_decode(filepath, head, sizeof(head), tail, sizeof(tail), &numlen);
BLI_path_sequence_encode(filepath, sizeof(filepath), head, tail, numlen, framenr);
}
return filepath;
}
/**
* 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 bNode *find_bsdf_node(const bNodeTree *nodetree)
{
if (!nodetree) {
return nullptr;
}
for (const bNode *node : nodetree->nodes_by_type("ShaderNodeOutputMaterial"_ustr)) {
const bNodeSocket &node_input_socket0 = node->input_socket(0);
for (const bNodeSocket *out_sock : node_input_socket0.directly_linked_sockets()) {
const bNode &in_node = out_sock->owner_node();
if (in_node.typeinfo->type_legacy == 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 bNode *bsdf_node,
const Material *material,
MTLMaterial &r_mtl_mat)
{
float roughness = material->roughness;
if (bsdf_node) {
copy_property_from_node(SOCK_FLOAT, bsdf_node, "Roughness", {&roughness, 1});
}
/* Empirical approximation. Importer should use the inverse of this method. */
float spec_exponent = (1.0f - roughness);
spec_exponent *= spec_exponent * 1000.0f;
float specular = material->spec;
if (bsdf_node) {
copy_property_from_node(SOCK_FLOAT, bsdf_node, "Specular IOR Level", {&specular, 1});
}
float metallic = material->metallic;
if (bsdf_node) {
copy_property_from_node(SOCK_FLOAT, bsdf_node, "Metallic", {&metallic, 1});
}
float refraction_index = 1.0f;
if (bsdf_node) {
copy_property_from_node(SOCK_FLOAT, bsdf_node, "IOR", {&refraction_index, 1});
}
float alpha = material->a;
if (bsdf_node) {
copy_property_from_node(SOCK_FLOAT, bsdf_node, "Alpha", {&alpha, 1});
}
const bool transparent = alpha != 1.0f;
float3 diffuse_col = {material->r, material->g, material->b};
if (bsdf_node) {
copy_property_from_node(SOCK_RGBA, bsdf_node, "Base Color", {diffuse_col, 3});
}
float3 emission_col{0.0f};
float emission_strength = 0.0f;
if (bsdf_node) {
copy_property_from_node(SOCK_FLOAT, bsdf_node, "Emission Strength", {&emission_strength, 1});
copy_property_from_node(SOCK_RGBA, bsdf_node, "Emission Color", {emission_col, 3});
}
mul_v3_fl(emission_col, emission_strength);
float sheen = -1.0f;
float coat = -1.0f;
float coat_roughness = -1.0f;
float aniso = -1.0f;
float aniso_rot = -1.0f;
float transmission = -1.0f;
if (bsdf_node) {
copy_property_from_node(SOCK_FLOAT, bsdf_node, "Sheen Weight", {&sheen, 1});
copy_property_from_node(SOCK_FLOAT, bsdf_node, "Coat Weight", {&coat, 1});
copy_property_from_node(SOCK_FLOAT, bsdf_node, "Coat Roughness", {&coat_roughness, 1});
copy_property_from_node(SOCK_FLOAT, bsdf_node, "Anisotropic", {&aniso, 1});
copy_property_from_node(SOCK_FLOAT, bsdf_node, "Anisotropic Rotation", {&aniso_rot, 1});
copy_property_from_node(SOCK_FLOAT, bsdf_node, "Transmission Weight", {&transmission, 1});
/* Clearcoat used to include an implicit 0.25 factor, so stay compatible to old versions. */
coat *= 4.0f;
}
/* 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.spec_exponent = spec_exponent;
if (metallic != 0.0f) {
r_mtl_mat.ambient_color = {metallic, metallic, metallic};
}
else {
r_mtl_mat.ambient_color = {1.0f, 1.0f, 1.0f};
}
r_mtl_mat.color = diffuse_col;
r_mtl_mat.spec_color = {specular, specular, specular};
r_mtl_mat.emission_color = emission_col;
r_mtl_mat.ior = refraction_index;
r_mtl_mat.alpha = alpha;
r_mtl_mat.illum_mode = illum;
r_mtl_mat.roughness = roughness;
r_mtl_mat.metallic = metallic;
r_mtl_mat.sheen = sheen;
r_mtl_mat.cc_thickness = coat;
r_mtl_mat.cc_roughness = coat_roughness;
r_mtl_mat.aniso = aniso;
r_mtl_mat.aniso_rot = aniso_rot;
r_mtl_mat.transmit_color = {transmission, transmission, transmission};
}
/**
* Store image texture options and file-paths in `r_mtl_mat`.
*/
static void store_image_textures(const bNode *bsdf_node,
const bNodeTree *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;
}
/* 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 (int key = 0; key < int(MTLTexMapType::Count); ++key) {
MTLTexMap &value = r_mtl_mat.texture_maps[key];
Vector<const bNodeSocket *> linked_sockets;
const bNode *normal_map_node{nullptr};
if (key == int(MTLTexMapType::Normal)) {
/* Find sockets linked to destination "Normal" socket in P-BSDF node. */
linked_sockets_to_dest_id(bsdf_node, *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 {
/* Skip emission map if emission strength is zero. */
if (key == int(MTLTexMapType::Emission)) {
float emission_strength = 0.0f;
copy_property_from_node(
SOCK_FLOAT, bsdf_node, "Emission Strength", {&emission_strength, 1});
if (emission_strength == 0.0f) {
continue;
}
}
/* Find sockets linked to the destination socket of interest, in P-BSDF node. */
linked_sockets_to_dest_id(
bsdf_node, *node_tree, tex_map_type_to_socket_id[key], 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 std::string tex_image_filepath = get_image_filepath(tex_node);
if (tex_image_filepath.empty()) {
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.normal_strength, 1});
}
/* Texture transform options. Only translation (origin offset, "-o") and scale
* ("-o") are supported. */
copy_property_from_node(SOCK_VECTOR, mapping, "Location", {value.translation, 3});
copy_property_from_node(SOCK_VECTOR, mapping, "Scale", {value.scale, 3});
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 bNodeTree *nodetree = material->nodetree;
if (nodetree != nullptr) {
nodetree->ensure_topology_cache();
}
const bNode *bsdf_node = find_bsdf_node(nodetree);
store_bsdf_properties(bsdf_node, material, mtlmat);
store_image_textures(bsdf_node, nodetree, material, mtlmat);
return mtlmat;
}
} // namespace io::obj
} // namespace blender

View File

@@ -0,0 +1,91 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "BLI_math_vector_types.hh"
#include "DNA_node_types.h"
namespace blender {
struct Material;
namespace io::obj {
enum class MTLTexMapType {
Color = 0,
Metallic,
Specular,
SpecularExponent,
Roughness,
Sheen,
Reflection,
Emission,
Alpha,
Normal,
Count
};
extern const char *tex_map_type_to_socket_id[];
struct MTLTexMap {
bool is_valid() const
{
return !image_path.empty();
}
/* Target socket which this texture node connects to. */
float3 translation{0.0f};
float3 scale{1.0f};
/* Only Flat and Sphere 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 an `.MTL` file.
*/
struct MTLMaterial {
const MTLTexMap &tex_map_of_type(MTLTexMapType key) const
{
return texture_maps[int(key)];
}
MTLTexMap &tex_map_of_type(MTLTexMapType key)
{
return texture_maps[int(key)];
}
std::string name;
/* Always check for negative values while importing or exporting. Use defaults if
* any value is negative. */
float spec_exponent{-1.0f}; /* `Ns` */
float3 ambient_color{-1.0f}; /* `Ka` */
float3 color{-1.0f}; /* `Kd` */
float3 spec_color{-1.0f}; /* `Ks` */
float3 emission_color{-1.0f}; /* `Ke` */
float ior{-1.0f}; /* `Ni` */
float alpha{-1.0f}; /* `d` */
float3 transmit_color{-1.0f}; /* `Kt` / `Tf` */
float roughness{-1.0f}; /* `Pr` */
float metallic{-1.0f}; /* `Pm` */
float sheen{-1.0f}; /* `Ps` */
float cc_thickness{-1.0f}; /* `Pc` */
float cc_roughness{-1.0f}; /* `Pcr` */
float aniso{-1.0f}; /* `aniso` */
float aniso_rot{-1.0f}; /* `anisor` */
int illum_mode{-1};
MTLTexMap texture_maps[int(MTLTexMapType::Count)];
/* Only used for Normal Map node: `map_Bump`. */
float normal_strength{-1.0f};
};
MTLMaterial mtlmaterial_for_material(const Material *material);
} // namespace io::obj
} // namespace blender

View File

@@ -0,0 +1,273 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include <numeric>
#include "BLI_listbase.h"
#include "BLI_utility_mixins.hh"
#include "BKE_curve_legacy_convert.hh"
#include "BKE_curves.hh"
#include "DNA_curve_types.h"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_query.hh"
#include "obj_export_nurbs.hh"
namespace blender::io::obj {
/* -------------------------------------------------------------------- */
/** \name Utility
* \{ */
/**
* Find the multiplicity entry with the valid span occurring on the right side of the related
* break-point/knot.
*/
static int find_leftmost_span(const int8_t order, const Span<int> multiplicity)
{
int index = -1;
int acc = 0;
while (acc < order) {
acc += multiplicity[++index];
}
BLI_assert(index > -1);
return index;
}
/**
* Find the multiplicity entry with the valid span occurring on the left side of the related
* break-point/knot.
*/
static int find_rightmost_span(const int8_t order, const Span<int> multiplicity)
{
int index = multiplicity.size();
int acc = 0;
while (acc < order) {
acc += multiplicity[--index];
}
BLI_assert(index < multiplicity.size());
return index;
}
Span<float> valid_nurb_control_point_range(const int8_t order,
const Span<float> knots,
IndexRange &point_range)
{
/* No consideration for cyclic, export must expand the knot vector! */
BLI_assert(knots.size() == bke::curves::nurbs::knots_num(point_range.size(), order, false));
/* This assumes multiplicity < order * 2 */
const int order2 = order * 2;
Vector<int> left_mult = bke::curves::nurbs::calculate_multiplicity_sequence(
knots.slice(0, order2));
Vector<int> right_mult = bke::curves::nurbs::calculate_multiplicity_sequence(
knots.slice(knots.size() - order2, order2));
const int leftmost = find_leftmost_span(order, left_mult);
const int rightmost = find_rightmost_span(order, right_mult);
/* For reasonable curve knots they should add up to 0 */
const int acc_start = std::accumulate(left_mult.begin(), left_mult.begin() + leftmost + 1, 0);
const int acc_end = std::accumulate(&right_mult[rightmost], right_mult.end(), 0);
int skip_start = acc_start - order;
int skip_end = acc_end - order;
/* Update ranges */
point_range = point_range.drop_front(skip_start).drop_back(skip_end);
return knots.drop_front(skip_start).drop_back(skip_end);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name OBJCurves
* \{ */
OBJCurves::OBJCurves(const bke::CurvesGeometry &curve,
const float4x4 &transform,
const std::string &name)
: curve_(curve), transform_(transform), name_(name)
{
}
const float4x4 &OBJCurves::object_transform() const
{
return transform_;
}
const char *OBJCurves::get_curve_name() const
{
return name_.c_str();
}
int OBJCurves::total_splines() const
{
return curve_.curve_num;
}
int OBJCurves::total_spline_vertices(int spline_index) const
{
return curve_.points_by_curve()[spline_index].size();
}
int OBJCurves::num_control_points_u(int spline_index) const
{
return bke::curves::nurbs::control_points_num(curve_.points_by_curve()[spline_index].size(),
get_nurbs_degree_u(spline_index) + 1,
get_cyclic_u(spline_index));
}
int OBJCurves::num_control_points_v(int /*spline_index*/) const
{
return 1;
}
int OBJCurves::get_nurbs_degree_u(int spline_index) const
{
return curve_.nurbs_orders()[spline_index] - 1;
}
int OBJCurves::get_nurbs_degree_v(int /*spline_index*/) const
{
return -1;
}
bool OBJCurves::get_cyclic_u(int spline_index) const
{
return curve_.cyclic()[spline_index];
}
Span<float> OBJCurves::get_knots_u(int spline_index, Vector<float> &knot_buffer) const
{
const int point_count = curve_.points_by_curve()[spline_index].size();
const int8_t order = curve_.nurbs_orders()[spline_index];
const bool cyclic = curve_.cyclic()[spline_index];
const KnotsMode mode = KnotsMode(curve_.nurbs_knots_modes()[spline_index]);
const int knot_count = bke::curves::nurbs::knots_num(point_count, order, cyclic);
knot_buffer.resize(knot_count);
bke::curves::nurbs::calculate_knots(point_count, mode, order, cyclic, knot_buffer);
return knot_buffer;
}
Span<float3> OBJCurves::vertex_coordinates(int spline_index,
Vector<float3> & /*dynamic_point_buffer*/) const
{
const IndexRange point_range = curve_.points_by_curve()[spline_index];
return curve_.positions().slice(point_range);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name OBJLegacyCurve
* \{ */
OBJLegacyCurve::OBJLegacyCurve(const Depsgraph *depsgraph, Object *curve_object)
: export_object_eval_(curve_object)
{
export_object_eval_ = DEG_get_evaluated(depsgraph, curve_object);
export_curve_ = id_cast<Curve *>(export_object_eval_->data);
}
const Nurb *OBJLegacyCurve::get_spline(const int spline_index) const
{
return static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index));
}
const char *OBJLegacyCurve::get_curve_name() const
{
return export_object_eval_->id.name + 2;
}
int OBJLegacyCurve::total_splines() const
{
return export_curve_->nurb.count();
}
const float4x4 &OBJLegacyCurve::object_transform() const
{
return export_object_eval_->object_to_world();
}
int OBJLegacyCurve::total_spline_vertices(const int spline_index) const
{
const Nurb *const nurb = get_spline(spline_index);
return nurb->pntsu * nurb->pntsv;
}
Span<float3> OBJLegacyCurve::vertex_coordinates(const int spline_index,
Vector<float3> &dynamic_point_buffer) const
{
const Nurb *const nurb = get_spline(spline_index);
dynamic_point_buffer.resize(nurb->pntsu);
for (int64_t i = nurb->pntsu - 1; i >= 0; --i) {
const BPoint &bpoint = nurb->bp[i];
copy_v3_v3(dynamic_point_buffer[i], bpoint.vec);
}
return dynamic_point_buffer.as_span();
}
int OBJLegacyCurve::num_control_points_u(int spline_index) const
{
const Nurb *const nurb = get_spline(spline_index);
return bke::curves::nurbs::control_points_num(
nurb->pntsu, get_nurbs_degree_u(spline_index) + 1, get_cyclic_u(spline_index));
}
int OBJLegacyCurve::num_control_points_v(int spline_index) const
{
const Nurb *const nurb = get_spline(spline_index);
return nurb->pntsv;
}
int OBJLegacyCurve::get_nurbs_degree_u(const int spline_index) const
{
const Nurb *const nurb = get_spline(spline_index);
return nurb->type == CU_POLY ? 1 : nurb->orderu - 1;
}
int OBJLegacyCurve::get_nurbs_degree_v(const int spline_index) const
{
const Nurb *const nurb = get_spline(spline_index);
return nurb->type == CU_POLY ? 1 : nurb->orderv - 1;
}
bool OBJLegacyCurve::get_cyclic_u(int spline_index) const
{
const Nurb *const nurb = get_spline(spline_index);
return bool(nurb->flagu & CU_NURB_CYCLIC);
}
Span<float> OBJLegacyCurve::get_knots_u(int spline_index, Vector<float> &knot_buffer) const
{
const Nurb *const nurb = get_spline(spline_index);
const short flag = nurb->flagu;
const int8_t order = get_nurbs_degree_u(spline_index) + 1; /* Use utility in case of POLY */
const bool cyclic = flag & CU_NURB_CYCLIC;
const int knot_count = bke::curves::nurbs::knots_num(nurb->pntsu, order, cyclic);
if (flag & CU_NURB_CUSTOM) {
return Span<float>(nurb->knotsu, knot_count);
}
knot_buffer.resize(knot_count);
bke::curves::nurbs::calculate_knots(
nurb->pntsu, bke::knots_mode_from_legacy(flag), order, cyclic, knot_buffer);
return knot_buffer;
}
/** \} */
} // namespace blender::io::obj

View File

@@ -0,0 +1,149 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "BLI_math_matrix_types.hh"
#include "BLI_span.hh"
#include "BLI_vector.hh"
namespace blender {
struct Curve;
struct Nurb;
struct OBJExportParams;
struct Object;
struct Depsgraph;
namespace bke {
class CurvesGeometry;
}
namespace io::obj {
/**
* Finds the range within the control points that represents the sequence of valid spans or
* 'segments'.
*
* For example, if a NURBS curve of order 2 has following 5 knots:
* [0, 0, 0, 1, 1]
* associated to three control points. Valid control point range would be
* the interval [1, 2] and the knot sequence [0, 0, 1, 1] since the first
* knot/point does not contribute to any span/segment.
*/
Span<float> valid_nurb_control_point_range(int8_t order,
Span<float> knots,
IndexRange &point_range);
/**
* Curve object wrapper providing access to the a Curve Object's properties.
* Curve objects can contain multiple individual splines.
*/
class IOBJCurve {
public:
virtual ~IOBJCurve() = default;
virtual const float4x4 &object_transform() const = 0;
virtual const char *get_curve_name() const = 0;
/**
* Number of splines associated with the Curve object.assign_if_different
*/
virtual int total_splines() const = 0;
/**
* \param spline_index: Zero-based index of spline of interest.
* \return Total vertices in a spline.
*/
virtual int total_spline_vertices(int spline_index) const = 0;
/**
* Get the number of control points on the U-dimension.
*/
virtual int num_control_points_u(int spline_index) const = 0;
/**
* Get the number of control points on the V-dimension.
*/
virtual int num_control_points_v(int spline_index) const = 0;
/**
* Get the degree of the NURBS spline for the U-dimension.
*/
virtual int get_nurbs_degree_u(int spline_index) const = 0;
/**
* Get the degree of the NURBS spline for the V-dimension.
*/
virtual int get_nurbs_degree_v(int spline_index) const = 0;
/**
* True if the indexed spline is cyclic along U dimension.
*/
virtual bool get_cyclic_u(int spline_index) const = 0;
/**
* Get the knot vector for the U-dimension. Computes knots using the buffer if necessary.
*/
virtual Span<float> get_knots_u(int spline_index, Vector<float> &buffer) const = 0;
/**
* Get coordinates for the (non-looped) spline control points.
*/
virtual Span<float3> vertex_coordinates(int spline_index,
Vector<float3> &dynamic_point_buffer) const = 0;
};
class OBJCurves : public IOBJCurve, NonCopyable {
private:
const bke::CurvesGeometry &curve_;
const float4x4 transform_;
const std::string name_;
public:
OBJCurves(const bke::CurvesGeometry &curve, const float4x4 &transform, const std::string &name);
~OBJCurves() override = default;
const float4x4 &object_transform() const override;
const char *get_curve_name() const override;
int total_splines() const override;
int total_spline_vertices(int spline_index) const override;
int num_control_points_u(int spline_index) const override;
int num_control_points_v(int spline_index) const override;
int get_nurbs_degree_u(int spline_index) const override;
int get_nurbs_degree_v(int spline_index) const override;
bool get_cyclic_u(int spline_index) const override;
Span<float> get_knots_u(int spline_index, Vector<float> &buffer) const override;
Span<float3> vertex_coordinates(int spline_index,
Vector<float3> &dynamic_point_buffer) const override;
};
class OBJLegacyCurve : public IOBJCurve, NonCopyable {
private:
const Object *export_object_eval_;
const Curve *export_curve_;
const Nurb *get_spline(int spline_index) const;
public:
OBJLegacyCurve(const Depsgraph *depsgraph, Object *curve_object);
~OBJLegacyCurve() override = default;
const float4x4 &object_transform() const override;
const char *get_curve_name() const override;
int total_splines() const override;
int total_spline_vertices(int spline_index) const override;
int num_control_points_u(int spline_index) const override;
int num_control_points_v(int spline_index) const override;
int get_nurbs_degree_u(int spline_index) const override;
int get_nurbs_degree_v(int spline_index) const override;
bool get_cyclic_u(int spline_index) const override;
Span<float> get_knots_u(int spline_index, Vector<float> &buffer) const override;
Span<float3> vertex_coordinates(int spline_index,
Vector<float3> &dynamic_point_buffer) const override;
};
} // namespace io::obj
} // namespace blender

View File

@@ -0,0 +1,422 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include <cstdio>
#include <memory>
#include <system_error>
#include "DNA_collection_types.h"
#include "DNA_curve_enums.h"
#include "DNA_curve_types.h"
#include "DNA_layer_types.h"
#include "DNA_scene_types.h"
#include "BKE_context.hh"
#include "BKE_lib_id.hh"
#include "BKE_report.hh"
#include "BKE_scene.hh"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BLI_task.hh"
#include "BLI_vector.hh"
#include "DEG_depsgraph_query.hh"
#include "ED_object.hh"
#include "obj_export_mesh.hh"
#include "obj_export_nurbs.hh"
#include "obj_exporter.hh"
#include "obj_export_file_writer.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.obj"};
namespace io::obj {
OBJDepsgraph::OBJDepsgraph(const bContext *C,
const eEvaluationMode eval_mode,
Collection *collection)
{
Scene *scene = CTX_data_scene(C);
Main *bmain = CTX_data_main(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
/* If a collection was provided, use it. */
if (collection) {
depsgraph_ = DEG_graph_new(bmain, scene, view_layer, eval_mode);
needs_free_ = true;
DEG_graph_build_from_collection(depsgraph_, collection);
BKE_scene_graph_evaluated_ensure(depsgraph_, bmain);
}
else if (eval_mode == DAG_EVAL_RENDER) {
depsgraph_ = DEG_graph_new(bmain, scene, view_layer, eval_mode);
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)
{
CLOG_ERROR(&LOG, "[%s] %s", ex.code().category().name(), ex.what());
}
static bool is_curve_nurbs_compatible(const Nurb *nurb)
{
while (nurb) {
if (nurb->type == CU_BEZIER || nurb->pntsv != 1) {
return false;
}
nurb = nurb->next;
}
return true;
}
/**
* 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<IOBJCurve>>>
filter_supported_objects(Depsgraph *depsgraph, const OBJExportParams &export_params)
{
Vector<std::unique_ptr<OBJMesh>> r_exportable_meshes;
Vector<std::unique_ptr<IOBJCurve>> r_exportable_nurbs;
DEGObjectIterSettings deg_iter_settings{};
deg_iter_settings.depsgraph = depsgraph;
deg_iter_settings.flags = DEG_ITER_OBJECT_FLAG_LINKED_DIRECTLY |
DEG_ITER_OBJECT_FLAG_LINKED_VIA_SET | DEG_ITER_OBJECT_FLAG_VISIBLE |
DEG_ITER_OBJECT_FLAG_DUPLI;
DEG_OBJECT_ITER_BEGIN (&deg_iter_settings, object) {
if (export_params.export_selected_objects && !(object->base_flag & BASE_SELECTED)) {
continue;
}
switch (object->type) {
case OB_SURF:
/* Evaluated surface objects appear as mesh objects from the iterator. */
break;
case OB_MESH:
r_exportable_meshes.append(std::make_unique<OBJMesh>(depsgraph, export_params, object));
break;
case OB_CURVES_LEGACY: {
Curve *curve = id_cast<Curve *>(object->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) {
IOBJCurve *obj_curve = new OBJLegacyCurve(depsgraph, object);
r_exportable_nurbs.append(std::unique_ptr<IOBJCurve>(obj_curve));
}
break;
}
if (export_params.export_curves_as_nurbs && is_curve_nurbs_compatible(nurb)) {
/* Export in parameter form: control points. */
IOBJCurve *obj_curve = new OBJLegacyCurve(depsgraph, object);
r_exportable_nurbs.append(std::unique_ptr<IOBJCurve>(obj_curve));
}
else {
/* Export in mesh form: edges and vertices. */
r_exportable_meshes.append(std::make_unique<OBJMesh>(depsgraph, export_params, object));
}
break;
}
default:
/* Other object types are not supported. */
break;
}
}
DEG_OBJECT_ITER_END;
return {std::move(r_exportable_meshes), std::move(r_exportable_nurbs)};
}
static void write_mesh_objects(const Span<std::unique_ptr<OBJMesh>> exportable_as_mesh,
OBJWriter &obj_writer,
MTLWriter *mtl_writer,
const OBJExportParams &export_params)
{
/* Parallelization is over meshes/objects, which means
* we have to have the output text buffer for each object,
* and write them all into the file at the end. */
size_t count = exportable_as_mesh.size();
Array<FormatHandler> buffers(count);
/* Serial: gather material indices, ensure normals & edges. */
Vector<Vector<int>> mtlindices;
if (mtl_writer) {
if (export_params.export_materials) {
obj_writer.write_mtllib_name(mtl_writer->mtl_file_path());
}
mtlindices.reserve(count);
}
for (const auto &obj_mesh : exportable_as_mesh) {
OBJMesh &obj = *obj_mesh;
if (mtl_writer) {
mtlindices.append(mtl_writer->add_materials(obj));
}
}
/* Parallel over meshes: store normal coords & indices, uv coords and indices. */
threading::parallel_for(IndexRange(count), 1, [&](IndexRange range) {
for (const int i : range) {
OBJMesh &obj = *exportable_as_mesh[i];
if (export_params.export_normals) {
obj.store_normal_coords_and_indices();
}
if (export_params.export_uv) {
obj.store_uv_coords_and_indices();
}
}
});
/* Serial: calculate index offsets; these are sequentially added
* over all meshes, and requite normal/uv indices to be calculated. */
Vector<IndexOffsets> index_offsets;
index_offsets.reserve(count);
IndexOffsets offsets{0, 0, 0};
for (const auto &obj_mesh : exportable_as_mesh) {
OBJMesh &obj = *obj_mesh;
index_offsets.append(offsets);
offsets.vertex_offset += obj.tot_vertices();
offsets.uv_vertex_offset += obj.tot_uv_vertices();
offsets.normal_offset += obj.get_normal_coords().size();
}
/* Parallel over meshes: main result writing. */
threading::parallel_for(IndexRange(count), 1, [&](IndexRange range) {
for (const int i : range) {
OBJMesh &obj = *exportable_as_mesh[i];
auto &fh = buffers[i];
obj_writer.write_object_name(fh, obj);
obj_writer.write_vertex_coords(fh, obj, export_params.export_colors);
if (obj.tot_faces() > 0) {
if (export_params.export_smooth_groups) {
obj.calc_smooth_groups(export_params.smooth_groups_bitflags);
}
if (export_params.export_materials) {
obj.calc_face_order();
}
if (export_params.export_normals) {
obj_writer.write_normals(fh, obj);
}
if (export_params.export_uv) {
obj_writer.write_uv_coords(fh, obj);
}
/* 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. */
const auto *obj_mtlindices = mtlindices.is_empty() ? nullptr : &mtlindices[i];
auto matname_fn = [&](int s) -> const char * {
if (!obj_mtlindices || s < 0 || s >= obj_mtlindices->size()) {
return nullptr;
}
return mtl_writer->mtlmaterial_name((*obj_mtlindices)[s]);
};
obj_writer.write_face_elements(fh, index_offsets[i], obj, matname_fn);
}
obj_writer.write_edges_indices(fh, index_offsets[i], obj);
/* Nothing will need this object's data after this point, release
* various arrays here. */
obj.clear();
}
});
/* Write all the object text buffers into the output file. */
FILE *f = obj_writer.get_outfile();
for (auto &b : buffers) {
b.write_to_file(f);
}
}
/**
* Export NURBS Curves in parameter form, not as vertices and edges.
*/
static void write_nurbs_curve_objects(const Span<std::unique_ptr<IOBJCurve>> exportable_as_nurbs,
const OBJWriter &obj_writer)
{
FormatHandler fh;
/* #OBJCurve doesn't have any dynamically allocated memory, so it's fine
* to wait for #Vector to clean the objects up. */
for (const std::unique_ptr<IOBJCurve> &obj_curve : exportable_as_nurbs) {
obj_writer.write_nurbs_curve(fh, *obj_curve);
}
fh.write_to_file(obj_writer.get_outfile());
}
static bool open_stream_writers(const OBJExportParams &export_params,
const char *filepath,
std::unique_ptr<OBJWriter> &r_frame_writer,
std::unique_ptr<MTLWriter> &r_mtl_writer)
{
try {
r_frame_writer = std::make_unique<OBJWriter>(filepath, export_params);
}
catch (const std::system_error &ex) {
print_exception_error(ex);
BKE_reportf(export_params.reports, RPT_ERROR, "OBJ Export: Cannot open file '%s'", filepath);
return false;
}
if (!r_frame_writer) {
BLI_assert_msg(false, "File should be writable by now.");
return false;
}
if (export_params.export_materials || export_params.export_material_groups) {
try {
r_mtl_writer = std::make_unique<MTLWriter>(filepath, export_params.export_materials);
}
catch (const std::system_error &ex) {
print_exception_error(ex);
BKE_reportf(export_params.reports,
RPT_WARNING,
"OBJ Export: Cannot create mtl file for '%s'",
filepath);
}
}
return true;
}
static void write_materials(MTLWriter *mtl_writer, const OBJExportParams &export_params)
{
BLI_assert(mtl_writer);
mtl_writer->write_header(export_params.blen_filepath);
char dest_dir[FILE_MAX];
if (export_params.file_base_for_tests[0] == '\0') {
BLI_path_split_dir_part(export_params.filepath, dest_dir, sizeof(dest_dir));
}
else {
STRNCPY(dest_dir, export_params.file_base_for_tests);
}
BLI_path_slash_native(dest_dir);
BLI_path_normalize(dest_dir);
mtl_writer->write_materials(export_params.blen_filepath,
export_params.path_mode,
dest_dir,
export_params.export_pbr_extensions);
}
void export_objects(const OBJExportParams &export_params,
const Span<std::unique_ptr<OBJMesh>> meshes,
const Span<std::unique_ptr<IOBJCurve>> curves,
const char *filepath)
{
/* Open */
std::unique_ptr<OBJWriter> obj_writer;
std::unique_ptr<MTLWriter> mtl_writer;
if (!open_stream_writers(export_params, filepath, obj_writer, mtl_writer)) {
return;
}
/* Write */
obj_writer->write_header();
write_mesh_objects(meshes, *obj_writer, mtl_writer.get(), export_params);
write_nurbs_curve_objects(curves, *obj_writer);
if (mtl_writer && export_params.export_materials) {
write_materials(mtl_writer.get(), export_params);
}
}
void export_frame(Depsgraph *depsgraph, const OBJExportParams &export_params, const char *filepath)
{
auto [exportable_as_mesh, exportable_as_nurbs] = filter_supported_objects(depsgraph,
export_params);
if (exportable_as_mesh.size() == 0 && exportable_as_nurbs.size() == 0) {
BKE_reportf(export_params.reports, RPT_WARNING, "OBJ Export: No information to write");
return;
}
export_objects(export_params, exportable_as_mesh, exportable_as_nurbs, filepath);
}
bool append_frame_to_filename(const char *filepath,
const int frame,
char r_filepath_with_frames[FILE_MAX])
{
BLI_strncpy(r_filepath_with_frames, filepath, FILE_MAX);
BLI_path_extension_strip(r_filepath_with_frames);
BLI_path_frame(r_filepath_with_frames, FILE_MAX, frame, 4);
return BLI_path_extension_replace(r_filepath_with_frames, FILE_MAX, ".obj");
}
void exporter_main(bContext *C, const OBJExportParams &export_params)
{
ed::object::mode_set(C, OB_MODE_OBJECT);
Collection *collection = nullptr;
if (export_params.collection[0]) {
Main *bmain = CTX_data_main(C);
collection = reinterpret_cast<Collection *>(
BKE_libblock_find_name(bmain, ID_GR, export_params.collection));
if (!collection) {
BKE_reportf(export_params.reports,
RPT_ERROR,
"OBJ Export: Unable to find collection '%s'",
export_params.collection);
return;
}
}
OBJDepsgraph obj_depsgraph(C, export_params.export_eval_mode, collection);
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) {
fmt::println("Writing to {}", 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 = scene->r.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) {
CLOG_ERROR(&LOG, "File Path too long: %s", filepath_with_frames);
return;
}
scene->r.cfra = frame;
obj_depsgraph.update_for_newframe();
fmt::println("Writing to {}", filepath_with_frames);
export_frame(obj_depsgraph.get(), export_params, filepath_with_frames);
}
scene->r.cfra = original_frame;
}
} // namespace io::obj
} // namespace blender

View File

@@ -0,0 +1,101 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "BLI_utility_mixins.hh"
#include "BLI_vector.hh"
#include "IO_wavefront_obj.hh"
namespace blender {
struct bContext;
struct Collection;
namespace 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, eEvaluationMode eval_mode, Collection *collection);
~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.
*/
/**
* Central internal function to call Scene update & writer functions.
*/
void exporter_main(bContext *C, const OBJExportParams &export_params);
class OBJMesh;
class IOBJCurve;
/**
* 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.
*/
/**
* 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);
void export_objects(const OBJExportParams &export_params,
Span<std::unique_ptr<OBJMesh>> meshes,
Span<std::unique_ptr<IOBJCurve>> curves,
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_CURVES_LEGACY`, 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<IOBJCurve>>>
filter_supported_objects(Depsgraph *depsgraph, const OBJExportParams &export_params);
/**
* Append the current frame number in the `.OBJ` file name.
*
* \param r_filepath_with_frames: The result of the `filepath` with its "#" characters
* replaced by the number representing `frame`, and with an `.obj` extension.
*
* \return Whether the `filepath` is in #FILE_MAX limits.
*/
bool append_frame_to_filename(const char *filepath,
int frame,
char r_filepath_with_frames[/*FILE_MAX*/ 1024]);
} // namespace io::obj
} // namespace blender

View File

@@ -0,0 +1,117 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include "BKE_mesh.hh"
#include "BKE_object.hh"
#include "BLI_delaunay_2d.hh"
#include "BLI_math_geom.h"
#include "BLI_math_matrix.h"
#include "BLI_math_rotation.h"
#include "BLI_math_vector.h"
#include "DNA_object_types.h"
#include "IO_wavefront_obj.hh"
#include "importer_mesh_utils.hh"
#include <numeric>
namespace blender::io::obj {
Vector<Vector<int>> fixup_invalid_face(Span<float3> vert_positions, Span<int> face_verts)
{
using namespace blender::meshintersect;
if (face_verts.size() < 3) {
return {};
}
const float3 normal = bke::mesh::face_normal_calc(vert_positions, face_verts);
float axis_mat[3][3];
axis_dominant_v3_to_m3(axis_mat, normal);
/* Project vertices to 2D. */
Array<double2> input_verts(face_verts.size());
for (const int i : face_verts.index_range()) {
int idx = face_verts[i];
BLI_assert(idx >= 0 && idx < vert_positions.size());
float2 coord2d;
mul_v2_m3v3(coord2d, axis_mat, vert_positions[idx]);
input_verts[i] = double2(coord2d.x, coord2d.y);
}
Array<Vector<int>> input_faces(1);
input_faces.first().resize(input_verts.size());
std::iota(input_faces.first().begin(), input_faces.first().end(), 0);
/* Prepare data for CDT. */
CDT_input<double> input;
input.vert = std::move(input_verts);
input.face = std::move(input_faces);
input.epsilon = 1.0e-6f;
input.need_ids = true;
CDT_result<double> res = delaunay_2d_calc(input, CDT_CONSTRAINTS_VALID_BMESH_WITH_HOLES);
/* Emit new face information from CDT result. */
Vector<Vector<int>> faces;
faces.reserve(res.face.size());
for (const auto &res_face : res.face) {
Vector<int> res_face_verts;
res_face_verts.reserve(res_face.size());
for (int64_t i = 0; i < res_face.size(); ++i) {
int idx = res_face[i];
BLI_assert(idx >= 0 && idx < res.vert_orig.size());
if (res.vert_orig[idx].is_empty()) {
/* If we have a whole new vertex in the tessellated result, we won't quite know what to do
* with it (how to create normal/UV for it, for example). Such vertices are often due to
* self-intersecting faces. Just skip them from the output face. */
}
else {
/* Vertex corresponds to one or more of the input vertices, use it. */
idx = res.vert_orig[idx][0];
BLI_assert(idx >= 0 && idx < face_verts.size());
res_face_verts.append(idx);
}
}
faces.append(res_face_verts);
}
return faces;
}
void transform_object(Object *object, const OBJImportParams &import_params)
{
float axes_transform[3][3];
unit_m3(axes_transform);
float obmat[4][4];
unit_m4(obmat);
/* +Y-forward and +Z-up are the default Blender axis settings. */
mat3_from_axis_conversion(
IO_AXIS_Y, IO_AXIS_Z, import_params.forward_axis, import_params.up_axis, axes_transform);
copy_m4_m3(obmat, axes_transform);
float scale_vec[3] = {
import_params.global_scale, import_params.global_scale, import_params.global_scale};
rescale_m4(obmat, scale_vec);
BKE_object_apply_mat4(object, obmat, true, false);
}
std::string get_geometry_name(const std::string &full_name, char separator)
{
if (separator == 0) {
return full_name;
}
size_t pos = full_name.find_last_of(separator);
if (pos == std::string::npos) {
return full_name;
}
return full_name.substr(pos + 1);
}
} // namespace blender::io::obj

View File

@@ -0,0 +1,44 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "BLI_math_vector_types.hh"
#include "BLI_span.hh"
#include "BLI_vector.hh"
#include <string>
namespace blender {
struct Object;
struct OBJImportParams;
namespace io::obj {
/**
* Given an invalid face (with holes or duplicated vertex indices),
* turn it into possibly multiple faces that are valid.
*
* \param vert_positions: Polygon's vertex coordinate list.
* \param face_verts: A face's indices that index into the given vertex coordinate
* list.
*
* \return List of faces with each element containing indices of one face. The indices
* are into face_vert_indices array.
*/
Vector<Vector<int>> fixup_invalid_face(Span<float3> vert_positions, Span<int> face_verts);
/**
* Apply axes transform to the Object, and clamp object dimensions to the specified value.
*/
void transform_object(Object *object, const OBJImportParams &import_params);
std::string get_geometry_name(const std::string &full_name, char separator);
} // namespace io::obj
} // namespace blender

View File

@@ -0,0 +1,979 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include "BKE_report.hh"
#include "BLI_fileops.h"
#include "BLI_map.hh"
#include "BLI_math_color.h"
#include "BLI_math_vector.h"
#include "BLI_math_vector_types.hh"
#include "BLI_mmap.h"
#include "BLI_string.h"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "IO_string_utils.hh"
#include "IO_validate.hh"
#include "obj_export_mtl.hh"
#include "obj_import_file_reader.hh"
#include <algorithm>
#include <charconv>
#include <fcntl.h>
#ifndef WIN32
# include <unistd.h>
#else
# include <io.h>
#endif
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.obj"};
namespace io::obj {
using std::string;
/**
* Based on the properties of the given Geometry instance, create a new Geometry instance
* or return the previous one.
*/
static Geometry *create_geometry(Geometry *const prev_geometry,
const eGeometryType new_type,
StringRef name,
Vector<std::unique_ptr<Geometry>> &r_all_geometries)
{
auto new_geometry = [&]() {
r_all_geometries.append(std::make_unique<Geometry>());
Geometry *g = r_all_geometries.last().get();
g->geom_type_ = new_type;
g->geometry_name_ = name.is_empty() ? "New object" : name;
return g;
};
if (prev_geometry && prev_geometry->geom_type_ == GEOM_MESH) {
/* After the creation of a Geometry instance, at least one element has been found in the OBJ
* file that indicates that it is a mesh (faces or edges). */
if (!prev_geometry->face_elements_.is_empty() || !prev_geometry->edges_.is_empty()) {
return new_geometry();
}
if (new_type == GEOM_MESH) {
/* A Geometry created initially with a default name now found its name. */
prev_geometry->geometry_name_ = name;
return prev_geometry;
}
if (new_type == GEOM_CURVE) {
/* The object originally created is not a mesh now that curve data
* follows the vertex coordinates list. */
prev_geometry->geom_type_ = GEOM_CURVE;
return prev_geometry;
}
}
return new_geometry();
}
static void geom_add_vertex(const char *p, const char *end, GlobalVertices &r_global_vertices)
{
r_global_vertices.flush_mrgb_block();
float3 vert;
p = parse_floats(p, end, 0.0f, vert, 3);
r_global_vertices.vertices.append(vert);
/* OBJ extension: `xyzrgb` vertex colors, when the vertex position
* is followed by 3 more RGB color components. See
* http://paulbourke.net/dataformats/obj/colour.html */
if (p < end) {
float3 srgb;
p = parse_floats(p, end, -1.0f, srgb, 3);
if (srgb.x >= 0 && srgb.y >= 0 && srgb.z >= 0) {
float3 linear;
srgb_to_linearrgb_v3_v3(linear, srgb);
r_global_vertices.set_vertex_color(r_global_vertices.vertices.size() - 1, linear);
}
else if (srgb.x > 0) {
/* Treats value in srgb.x as weight. */
r_global_vertices.set_vertex_weight(r_global_vertices.vertices.size() - 1, srgb.x);
}
}
UNUSED_VARS(p);
}
static void geom_add_mrgb_colors(const char *p, const char *end, GlobalVertices &r_global_vertices)
{
/* MRGB color extension, in the form of
* "#MRGB MMRRGGBBMMRRGGBB ..."
* http://paulbourke.net/dataformats/obj/colour.html */
p = drop_whitespace(p, end);
const int mrgb_length = 8;
while (p + mrgb_length <= end) {
uint32_t value = 0;
std::from_chars_result res = std::from_chars(p, p + mrgb_length, value, 16);
if (ELEM(res.ec, std::errc::invalid_argument, std::errc::result_out_of_range)) {
return;
}
uchar srgb[4];
srgb[0] = (value >> 16) & 0xFF;
srgb[1] = (value >> 8) & 0xFF;
srgb[2] = value & 0xFF;
srgb[3] = 0xFF;
float linear[4];
srgb_to_linearrgb_uchar4(linear, srgb);
r_global_vertices.mrgb_block.append(float3(linear[0], linear[1], linear[2]));
p += mrgb_length;
}
}
static void geom_add_vertex_normal(const char *p,
const char *end,
GlobalVertices &r_global_vertices)
{
float3 normal;
parse_floats(p, end, 0.0f, normal, 3);
/* Normals can be printed with only several digits in the file,
* making them ever-so-slightly non unit length. Make sure they are
* normalized. */
normalize_v3(normal);
r_global_vertices.vert_normals.append(normal);
}
static void geom_add_uv_vertex(const char *p, const char *end, GlobalVertices &r_global_vertices)
{
float2 uv;
parse_floats(p, end, 0.0f, uv, 2);
r_global_vertices.uv_vertices.append(uv);
}
/**
* Parse vertex index and transform to non-negative, zero-based.
* Sets r_index to the index or INT32_MAX on error.
* Index is transformed and bounds-checked using n_vertices,
* which specifies the number of vertices that have been read before.
* Returns updated p.
*/
static const char *parse_vertex_index(const char *p, const char *end, size_t n_elems, int &r_index)
{
p = parse_int(p, end, INT32_MAX, r_index, false);
if (r_index != INT32_MAX) {
r_index += r_index < 0 ? n_elems : -1;
if (r_index < 0 || r_index >= n_elems) {
CLOG_WARN(&LOG, "Invalid vertex index %i (valid range [0, %zu))", r_index, n_elems);
r_index = INT32_MAX;
}
}
return p;
}
/**
* Parse a polyline and add its line segments as loose edges.
* We support the following polyline specifications:
* - "l v1/vt1 v2/vt2 ..."
* - "l v1 v2 ..."
* If a line only has one vertex (technically not allowed by the spec),
* no line is created, but the vertex will be added to
* the mesh even if it is unconnected.
*/
static void geom_add_polyline(Geometry *geom,
const char *p,
const char *end,
GlobalVertices &r_global_vertices)
{
int last_vertex_index;
p = drop_whitespace(p, end);
p = parse_vertex_index(p, end, r_global_vertices.vertices.size(), last_vertex_index);
if (last_vertex_index == INT32_MAX) {
CLOG_WARN(&LOG, "Skipping invalid OBJ polyline.");
return;
}
geom->track_vertex_index(last_vertex_index);
while (p < end) {
int vertex_index;
/* Lines can contain texture coordinate indices, just ignore them. */
p = drop_non_whitespace(p, end);
/* Skip whitespace to get to the next vertex. */
p = drop_whitespace(p, end);
p = parse_vertex_index(p, end, r_global_vertices.vertices.size(), vertex_index);
if (vertex_index == INT32_MAX) {
break;
}
geom->edges_.append({last_vertex_index, vertex_index});
geom->track_vertex_index(vertex_index);
last_vertex_index = vertex_index;
}
}
static void geom_add_polygon(Geometry *geom,
const char *p,
const char *end,
const GlobalVertices &global_vertices,
const int material_index,
const int group_index,
const bool shaded_smooth)
{
FaceElem curr_face;
curr_face.shaded_smooth = shaded_smooth;
curr_face.material_index = material_index;
if (group_index >= 0) {
curr_face.vertex_group_index = group_index;
geom->has_vertex_groups_ = true;
}
const int orig_corners_size = geom->face_corners_.size();
curr_face.start_index_ = orig_corners_size;
bool face_valid = true;
p = drop_whitespace(p, end);
while (p < end && face_valid) {
FaceCorner corner;
bool got_uv = false, got_normal = false;
/* Parse vertex index. */
p = parse_int(p, end, INT32_MAX, corner.vert_index, false);
/* Skip parsing when we reach start of the comment. */
if (*p == '#') {
break;
}
face_valid &= corner.vert_index != INT32_MAX;
if (p < end && *p == '/') {
/* Parse UV index. */
++p;
if (p < end && *p != '/') {
p = parse_int(p, end, INT32_MAX, corner.uv_vert_index, false);
got_uv = corner.uv_vert_index != INT32_MAX;
}
/* Parse normal index. */
if (p < end && *p == '/') {
++p;
p = parse_int(p, end, INT32_MAX, corner.vertex_normal_index, false);
got_normal = corner.vertex_normal_index != INT32_MAX;
}
}
/* Always keep stored indices non-negative and zero-based. */
corner.vert_index += corner.vert_index < 0 ? global_vertices.vertices.size() : -1;
if (corner.vert_index < 0 || corner.vert_index >= global_vertices.vertices.size()) {
CLOG_WARN(&LOG,
"Invalid vertex index %i (valid range [0, %zu)), ignoring face",
corner.vert_index,
size_t(global_vertices.vertices.size()));
face_valid = false;
}
else {
geom->track_vertex_index(corner.vert_index);
}
/* Ignore UV index, if the geometry does not have any UVs (#103212). */
if (got_uv && !global_vertices.uv_vertices.is_empty()) {
corner.uv_vert_index += corner.uv_vert_index < 0 ? global_vertices.uv_vertices.size() : -1;
if (corner.uv_vert_index < 0 || corner.uv_vert_index >= global_vertices.uv_vertices.size()) {
CLOG_WARN(&LOG,
"Invalid UV index %i (valid range [0, %zu)), ignoring face",
corner.uv_vert_index,
size_t(global_vertices.uv_vertices.size()));
face_valid = false;
}
}
/* Ignore corner normal index, if the geometry does not have any normals.
* Some obj files out there do have face definitions that refer to normal indices,
* without any normals being present (#98782). */
if (got_normal && !global_vertices.vert_normals.is_empty()) {
corner.vertex_normal_index += corner.vertex_normal_index < 0 ?
global_vertices.vert_normals.size() :
-1;
if (corner.vertex_normal_index < 0 ||
corner.vertex_normal_index >= global_vertices.vert_normals.size())
{
CLOG_WARN(&LOG,
"Invalid normal index %i (valid range [0, %zu)), ignoring face",
corner.vertex_normal_index,
size_t(global_vertices.vert_normals.size()));
face_valid = false;
}
}
geom->face_corners_.append(corner);
curr_face.corner_count_++;
/* Some files contain extra stuff per face (e.g. 4 indices); skip any remainder (#103441). */
p = drop_non_whitespace(p, end);
/* Skip whitespace to get to the next face corner. */
p = drop_whitespace(p, end);
}
if (face_valid) {
geom->face_elements_.append(curr_face);
geom->total_corner_ += curr_face.corner_count_;
}
else {
/* Remove just-added corners for the invalid face. */
geom->face_corners_.resize(orig_corners_size);
geom->has_invalid_faces_ = true;
}
}
static Geometry *geom_set_curve_type(Geometry *geom,
const char *p,
const char *end,
const StringRef group_name,
Vector<std::unique_ptr<Geometry>> &r_all_geometries)
{
p = drop_whitespace(p, end);
if (!StringRef(p, end).startswith("bspline") && !StringRef(p, end).startswith("rat bspline")) {
CLOG_WARN(&LOG, "Curve type not supported: '%s'", string(p, end).c_str());
return geom;
}
geom = create_geometry(geom, GEOM_CURVE, group_name, r_all_geometries);
geom->nurbs_element_.group_ = group_name;
return geom;
}
static void geom_set_curve_degree(Geometry *geom, const char *p, const char *end)
{
parse_int(p, end, 3, geom->nurbs_element_.degree);
}
static void geom_add_curve_vertex_indices(Geometry *geom,
const char *p,
const char *end,
const GlobalVertices &global_vertices)
{
/* Parse curve parameter range. */
p = parse_floats(p, end, 0, geom->nurbs_element_.range, 2);
/* Parse indices. */
while (p < end) {
int index;
p = parse_int(p, end, INT32_MAX, index);
if (index == INT32_MAX) {
return;
}
/* Always keep stored indices non-negative and zero-based. */
index += index < 0 ? global_vertices.vertices.size() : -1;
if (!validate::index_in_range(index, global_vertices.vertices.size())) {
index = 0;
}
geom->nurbs_element_.curv_indices.append(index);
}
}
static void geom_add_curve_parameters(Geometry *geom, const char *p, const char *end)
{
p = drop_whitespace(p, end);
if (p == end) {
CLOG_ERROR(&LOG, "Invalid OBJ curve parm line");
return;
}
if (*p != 'u') {
CLOG_WARN(&LOG, "OBJ curve surfaces are not supported, found '%c'", *p);
return;
}
++p;
while (p < end) {
float val;
p = parse_float(p, end, FLT_MAX, val);
if (val != FLT_MAX) {
geom->nurbs_element_.parm.append(val);
}
else {
CLOG_ERROR(&LOG, "OBJ curve parm line has invalid number");
return;
}
}
}
static void geom_update_group(const StringRef rest_line, string &r_group_name)
{
if (rest_line.find("off") != string::npos || rest_line.find("null") != string::npos ||
rest_line.find("default") != string::npos)
{
/* Set group for future elements like faces or curves to empty. */
r_group_name = "";
return;
}
r_group_name = rest_line;
}
static void geom_update_smooth_group(const char *p, const char *end, bool &r_state_shaded_smooth)
{
p = drop_whitespace(p, end);
/* Some implementations use "0" and "null" too, in addition to "off". */
const StringRef line = StringRef(p, end);
if (line == "0" || line.startswith("off") || line.startswith("null")) {
r_state_shaded_smooth = false;
return;
}
int smooth = 0;
parse_int(p, end, 0, smooth);
r_state_shaded_smooth = smooth != 0;
}
static void geom_new_object(const char *p,
const char *end,
bool &r_state_shaded_smooth,
string &r_state_group_name,
int &r_state_material_index,
Geometry *&r_curr_geom,
Vector<std::unique_ptr<Geometry>> &r_all_geometries)
{
r_state_shaded_smooth = false;
r_state_group_name = "";
/* Reset object-local material index that's used in face information.
* NOTE: do not reset the material name; that has to carry over
* into the next object if needed. */
r_state_material_index = -1;
r_curr_geom = create_geometry(
r_curr_geom, GEOM_MESH, StringRef(p, end).trim(), r_all_geometries);
}
OBJParser::OBJParser(const OBJImportParams &import_params) : import_params_(import_params)
{
const int obj_file = BLI_open(import_params_.filepath, O_BINARY | O_RDONLY, 0);
if (obj_file == -1) {
CLOG_ERROR(&LOG, "Cannot read from OBJ file:'%s'.", import_params_.filepath);
BKE_reportf(import_params_.reports,
RPT_ERROR,
"OBJ Import: Cannot open file '%s'",
import_params_.filepath);
return;
}
mmap_file_ = BLI_mmap_open(obj_file);
close(obj_file);
if (mmap_file_ == nullptr) {
CLOG_ERROR(&LOG, "Cannot mmap OBJ file:'%s'.", import_params_.filepath);
BKE_reportf(import_params_.reports,
RPT_ERROR,
"OBJ Import: Cannot mmap file '%s'",
import_params_.filepath);
return;
}
}
OBJParser::~OBJParser()
{
if (mmap_file_ != nullptr) {
BLI_mmap_free(mmap_file_);
}
}
/* If line starts with keyword followed by whitespace, returns true and drops it from the line. */
static bool parse_keyword(const char *&p, const char *end, StringRef keyword)
{
const size_t keyword_len = keyword.size();
if (end - p < keyword_len + 1) {
return false;
}
if (memcmp(p, keyword.data(), keyword_len) != 0) {
return false;
}
/* Treat any ASCII control character as white-space;
* don't use `isspace()` for performance reasons. */
if (p[keyword_len] > ' ') {
return false;
}
p += keyword_len + 1;
return true;
}
/* Special case: if there were no faces/edges in any geometries,
* treat all the vertices as a point cloud. */
static void use_all_vertices_if_no_faces(Geometry *geom,
const Span<std::unique_ptr<Geometry>> all_geometries,
const GlobalVertices &global_vertices)
{
if (!global_vertices.vertices.is_empty() && geom && geom->geom_type_ == GEOM_MESH) {
if (std::all_of(all_geometries.begin(),
all_geometries.end(),
[](const std::unique_ptr<Geometry> &g) { return g->get_vertex_count() == 0; }))
{
geom->track_all_vertices(global_vertices.vertices.size());
}
}
}
/* OBJ file format supports "line continuations", which
* are back-slashes, optionally followed by whitespace.
* The line virtually extends to the next line in that case. */
StringRef OBJParser::read_next_obj_line(StringRef &buffer)
{
const char *start = buffer.begin();
const char *end = buffer.end();
const char *ptr = start;
/* Scan until newline or backslash. */
while (ptr < end && *ptr != '\n' && *ptr != '\\') {
++ptr;
}
/* Common case: no backslash found, return reference to input data. */
if (ptr >= end || *ptr == '\n') {
size_t len = ptr - start;
buffer = StringRef(ptr < end ? ptr + 1 : ptr, end);
return StringRef(start, len);
}
/* We have backslash. Copy into line buffer, replace
* line continuation with space, return result. */
line_buffer_.assign(start, ptr);
while (ptr < end) {
char c = *ptr++;
if (c == '\\') {
/* Scan ahead, skipping whitespace until newline. */
const char *ahead = ptr;
while (ahead < end && *ahead <= ' ' && *ahead != '\n') {
++ahead;
}
if (ahead < end && *ahead == '\n') {
/* Line continuation: replace backslash & newline with space. */
line_buffer_ += ' ';
ptr = ahead + 1; /* Continue after the newline. */
}
else {
/* Not a continuation: keep the backslash. */
line_buffer_ += c;
}
}
else if (c == '\n') {
break;
}
else {
line_buffer_ += c;
}
}
buffer = StringRef(ptr, end);
return line_buffer_;
}
void OBJParser::parse_string_buffer(StringRef &buffer_str,
Vector<std::unique_ptr<Geometry>> &r_all_geometries,
GlobalVertices &r_global_vertices,
Geometry *&curr_geom)
{
/* State variables: once set, they remain the same for the remaining
* elements in the object. */
bool state_shaded_smooth = false;
string state_group_name;
int state_group_index = -1;
string state_material_name;
int state_material_index = -1;
while (!buffer_str.is_empty()) {
StringRef line = read_next_obj_line(buffer_str);
const char *p = line.begin(), *end = line.end();
p = drop_whitespace(p, end);
if (p == end) {
continue;
}
/* Most common things that start with 'v': vertices, normals, UVs. */
if (*p == 'v') {
if (parse_keyword(p, end, "v")) {
geom_add_vertex(p, end, r_global_vertices);
}
else if (parse_keyword(p, end, "vn")) {
geom_add_vertex_normal(p, end, r_global_vertices);
}
else if (parse_keyword(p, end, "vt")) {
geom_add_uv_vertex(p, end, r_global_vertices);
}
}
/* Faces. */
else if (parse_keyword(p, end, "f")) {
/* If we don't have a material index assigned yet, get one.
* It means "usemtl" state came from the previous object. */
if (state_material_index == -1 && !state_material_name.empty() &&
curr_geom->material_indices_.is_empty())
{
curr_geom->material_indices_.add_new(state_material_name, 0);
curr_geom->material_order_.append(state_material_name);
state_material_index = 0;
}
geom_add_polygon(curr_geom,
p,
end,
r_global_vertices,
state_material_index,
state_group_index,
state_shaded_smooth);
}
/* Faces. */
else if (parse_keyword(p, end, "l")) {
geom_add_polyline(curr_geom, p, end, r_global_vertices);
}
/* Objects. */
else if (parse_keyword(p, end, "o")) {
if (import_params_.use_split_objects) {
geom_new_object(p,
end,
state_shaded_smooth,
state_group_name,
state_material_index,
curr_geom,
r_all_geometries);
}
}
/* Groups. */
else if (parse_keyword(p, end, "g")) {
if (import_params_.use_split_groups) {
geom_new_object(p,
end,
state_shaded_smooth,
state_group_name,
state_material_index,
curr_geom,
r_all_geometries);
}
else {
geom_update_group(StringRef(p, end).trim(), state_group_name);
int new_index = curr_geom->group_indices_.size();
state_group_index = curr_geom->group_indices_.lookup_or_add(state_group_name, new_index);
if (new_index == state_group_index) {
curr_geom->group_order_.append(state_group_name);
}
}
}
/* Smoothing groups. */
else if (parse_keyword(p, end, "s")) {
geom_update_smooth_group(p, end, state_shaded_smooth);
}
/* Materials and their libraries. */
else if (parse_keyword(p, end, "usemtl")) {
state_material_name = StringRef(p, end).trim();
int new_mat_index = curr_geom->material_indices_.size();
state_material_index = curr_geom->material_indices_.lookup_or_add(state_material_name,
new_mat_index);
if (new_mat_index == state_material_index) {
curr_geom->material_order_.append(state_material_name);
}
}
else if (parse_keyword(p, end, "mtllib")) {
add_mtl_library(StringRef(p, end).trim());
}
else if (parse_keyword(p, end, "#MRGB")) {
geom_add_mrgb_colors(p, end, r_global_vertices);
}
/* Comments. */
else if (*p == '#') {
/* Nothing to do. */
}
/* Curve related things. */
else if (parse_keyword(p, end, "cstype")) {
curr_geom = geom_set_curve_type(curr_geom, p, end, state_group_name, r_all_geometries);
}
else if (parse_keyword(p, end, "deg")) {
geom_set_curve_degree(curr_geom, p, end);
}
else if (parse_keyword(p, end, "curv")) {
geom_add_curve_vertex_indices(curr_geom, p, end, r_global_vertices);
}
else if (parse_keyword(p, end, "parm")) {
geom_add_curve_parameters(curr_geom, p, end);
}
else if (StringRef(p, end).startswith("end")) {
/* End of curve definition, nothing else to do. */
}
else {
CLOG_WARN(&LOG, "OBJ element not recognized: '%s'", string(p, end).c_str());
}
}
}
void OBJParser::parse(Vector<std::unique_ptr<Geometry>> &r_all_geometries,
GlobalVertices &r_global_vertices)
{
if (!mmap_file_) {
return;
}
/* Use the filename as the default name given to the initial object. */
char ob_name[FILE_MAXFILE];
STRNCPY(ob_name, BLI_path_basename(import_params_.filepath));
BLI_path_extension_strip(ob_name);
Geometry *curr_geom = create_geometry(nullptr, GEOM_MESH, ob_name, r_all_geometries);
const char *file_data = static_cast<const char *>(BLI_mmap_get_pointer(mmap_file_));
size_t file_size = BLI_mmap_get_length(mmap_file_);
StringRef buffer_str{file_data, int64_t(file_size)};
OBJParser::parse_string_buffer(buffer_str, r_all_geometries, r_global_vertices, curr_geom);
r_global_vertices.flush_mrgb_block();
use_all_vertices_if_no_faces(curr_geom, r_all_geometries, r_global_vertices);
add_default_mtl_library();
}
static MTLTexMapType mtl_line_start_to_texture_type(const char *&p, const char *end)
{
if (parse_keyword(p, end, "map_Kd")) {
return MTLTexMapType::Color;
}
if (parse_keyword(p, end, "map_Ks")) {
return MTLTexMapType::Specular;
}
if (parse_keyword(p, end, "map_Ns")) {
return MTLTexMapType::SpecularExponent;
}
if (parse_keyword(p, end, "map_d")) {
return MTLTexMapType::Alpha;
}
if (parse_keyword(p, end, "refl") || parse_keyword(p, end, "map_refl")) {
return MTLTexMapType::Reflection;
}
if (parse_keyword(p, end, "map_Ke")) {
return MTLTexMapType::Emission;
}
if (parse_keyword(p, end, "bump") || parse_keyword(p, end, "map_Bump") ||
parse_keyword(p, end, "map_bump"))
{
return MTLTexMapType::Normal;
}
if (parse_keyword(p, end, "map_Pr")) {
return MTLTexMapType::Roughness;
}
if (parse_keyword(p, end, "map_Pm")) {
return MTLTexMapType::Metallic;
}
if (parse_keyword(p, end, "map_Ps")) {
return MTLTexMapType::Sheen;
}
return MTLTexMapType::Count;
}
static const std::pair<StringRef, int> unsupported_texture_options[] = {
{"-blendu", 1},
{"-blendv", 1},
{"-boost", 1},
{"-cc", 1},
{"-clamp", 1},
{"-imfchan", 1},
{"-mm", 2},
{"-t", 3},
{"-texres", 1},
};
static bool parse_texture_option(const char *&p,
const char *end,
MTLMaterial *material,
MTLTexMap &tex_map)
{
p = drop_whitespace(p, end);
if (parse_keyword(p, end, "-o")) {
p = parse_floats(p, end, 0.0f, tex_map.translation, 3, true);
return true;
}
if (parse_keyword(p, end, "-s")) {
p = parse_floats(p, end, 1.0f, tex_map.scale, 3, true);
return true;
}
if (parse_keyword(p, end, "-bm")) {
p = parse_float(p, end, 1.0f, material->normal_strength, true, true);
return true;
}
if (parse_keyword(p, end, "-type")) {
p = drop_whitespace(p, end);
/* Only sphere is supported. */
tex_map.projection_type = SHD_PROJ_SPHERE;
const StringRef line = StringRef(p, end);
if (!line.startswith("sphere")) {
CLOG_WARN(&LOG,
"Only the 'sphere' MTL projection type is supported, found: '%s'",
string(line).c_str());
}
p = drop_non_whitespace(p, end);
return true;
}
/* Check for unsupported options and skip them. */
for (const auto &opt : unsupported_texture_options) {
if (parse_keyword(p, end, opt.first)) {
/* Drop the arguments. */
for (int i = 0; i < opt.second; ++i) {
p = drop_whitespace(p, end);
p = drop_non_whitespace(p, end);
}
return true;
}
}
return false;
}
static void parse_texture_map(const char *p,
const char *end,
MTLMaterial *material,
const char *mtl_dir_path)
{
const StringRef line = StringRef(p, end);
bool is_map = line.startswith("map_");
bool is_refl = line.startswith("refl");
bool is_bump = line.startswith("bump");
if (!is_map && !is_refl && !is_bump) {
return;
}
MTLTexMapType key = mtl_line_start_to_texture_type(p, end);
if (key == MTLTexMapType::Count) {
/* No supported texture map found. */
CLOG_WARN(&LOG, "MTL texture map type not supported: '%s'", string(line).c_str());
return;
}
MTLTexMap &tex_map = material->tex_map_of_type(key);
tex_map.mtl_dir_path = mtl_dir_path;
/* Parse texture map options. */
while (parse_texture_option(p, end, material, tex_map)) {
}
/* What remains is the image path. */
tex_map.image_path = StringRef(p, end).trim();
}
Span<string> OBJParser::mtl_libraries() const
{
return mtl_libraries_;
}
void OBJParser::add_mtl_library(StringRef path)
{
/* Remove any quotes from start and end (#67266, #97794). */
if (path.size() > 2 && path.startswith("\"") && path.endswith("\"")) {
path = path.drop_prefix(1).drop_suffix(1);
}
if (!mtl_libraries_.contains(path)) {
mtl_libraries_.append(path);
}
}
void OBJParser::add_default_mtl_library()
{
/* Add any existing `.mtl` file that's with the same base name as the `.obj` file
* into candidate `.mtl` files to search through. This is not technically following the
* spec, but the old python importer was doing it, and there are user files out there
* that contain "mtllib bar.mtl" for a foo.obj, and depend on finding materials
* from foo.mtl (see #97757). */
char mtl_file_path[FILE_MAX];
STRNCPY(mtl_file_path, import_params_.filepath);
BLI_path_extension_replace(mtl_file_path, sizeof(mtl_file_path), ".mtl");
if (BLI_exists(mtl_file_path)) {
char mtl_file_base[FILE_MAX];
BLI_path_split_file_part(mtl_file_path, mtl_file_base, sizeof(mtl_file_base));
add_mtl_library(mtl_file_base);
}
}
MTLParser::MTLParser(StringRefNull mtl_library, StringRefNull obj_filepath)
{
char obj_file_dir[FILE_MAXDIR];
BLI_path_split_dir_part(obj_filepath.data(), obj_file_dir, FILE_MAXDIR);
BLI_path_join(mtl_file_path_, FILE_MAX, obj_file_dir, mtl_library.data());
/* Normalize the path to handle different paths pointing to the same file */
BLI_path_normalize(mtl_file_path_);
BLI_path_split_dir_part(mtl_file_path_, mtl_dir_path_, FILE_MAXDIR);
}
void MTLParser::parse_and_store(Map<string, std::unique_ptr<MTLMaterial>> &r_materials)
{
size_t buffer_len;
char *buffer = BLI_file_read_text_as_mem(mtl_file_path_, 0, &buffer_len);
if (buffer == nullptr) {
CLOG_ERROR(&LOG, "OBJ import: cannot read from MTL file: '%s'", mtl_file_path_);
return;
}
MTLMaterial *material = nullptr;
StringRef buffer_str{buffer, int64_t(buffer_len)};
while (!buffer_str.is_empty()) {
const StringRef line = read_next_line(buffer_str);
const char *p = line.begin(), *end = line.end();
p = drop_whitespace(p, end);
if (p == end) {
continue;
}
if (parse_keyword(p, end, "newmtl")) {
StringRef mat_name = StringRef(p, end).trim();
/* Always try to get or create the material, even if it already exists */
material =
r_materials.lookup_or_add(string(mat_name), std::make_unique<MTLMaterial>()).get();
}
else if (material != nullptr) {
if (parse_keyword(p, end, "Ns")) {
parse_float(p, end, 324.0f, material->spec_exponent);
}
else if (parse_keyword(p, end, "Ka")) {
parse_floats(p, end, 0.0f, material->ambient_color, 3);
}
else if (parse_keyword(p, end, "Kd")) {
parse_floats(p, end, 0.8f, material->color, 3);
}
else if (parse_keyword(p, end, "Ks")) {
parse_floats(p, end, 0.5f, material->spec_color, 3);
}
else if (parse_keyword(p, end, "Ke")) {
parse_floats(p, end, 0.0f, material->emission_color, 3);
}
else if (parse_keyword(p, end, "Ni")) {
parse_float(p, end, 1.45f, material->ior);
}
else if (parse_keyword(p, end, "d")) {
parse_float(p, end, 1.0f, material->alpha);
}
else if (parse_keyword(p, end, "illum")) {
/* Some files incorrectly use a float (#60135). */
float val;
parse_float(p, end, 1.0f, val);
material->illum_mode = val;
}
else if (parse_keyword(p, end, "Pr")) {
parse_float(p, end, 0.5f, material->roughness);
}
else if (parse_keyword(p, end, "Pm")) {
parse_float(p, end, 0.0f, material->metallic);
}
else if (parse_keyword(p, end, "Ps")) {
parse_float(p, end, 0.0f, material->sheen);
}
else if (parse_keyword(p, end, "Pc")) {
parse_float(p, end, 0.0f, material->cc_thickness);
}
else if (parse_keyword(p, end, "Pcr")) {
parse_float(p, end, 0.0f, material->cc_roughness);
}
else if (parse_keyword(p, end, "aniso")) {
parse_float(p, end, 0.0f, material->aniso);
}
else if (parse_keyword(p, end, "anisor")) {
parse_float(p, end, 0.0f, material->aniso_rot);
}
else if (parse_keyword(p, end, "Kt") || parse_keyword(p, end, "Tf")) {
parse_floats(p, end, 0.0f, material->transmit_color, 3);
}
else {
parse_texture_map(p, end, material, mtl_dir_path_);
}
}
}
MEM_delete(buffer);
}
} // namespace io::obj
} // namespace blender

View File

@@ -0,0 +1,80 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "IO_wavefront_obj.hh"
#include "BLI_map.hh"
#include "BLI_vector.hh"
#include "obj_import_objects.hh"
namespace blender {
struct BLI_mmap_file;
}
namespace blender::io::obj {
struct MTLMaterial;
class OBJParser {
private:
const OBJImportParams &import_params_;
Vector<std::string> mtl_libraries_;
BLI_mmap_file *mmap_file_ = nullptr;
std::string line_buffer_;
public:
/**
* Open OBJ file at the path given in import parameters.
*/
OBJParser(const OBJImportParams &import_params);
~OBJParser();
/**
* Read the OBJ file line by line and create OBJ Geometry instances. Also store all the vertex
* and UV vertex coordinates in a struct accessible by all objects.
*/
void parse(Vector<std::unique_ptr<Geometry>> &r_all_geometries,
GlobalVertices &r_global_vertices);
/**
* Return a list of all material library filepaths referenced by the OBJ file.
*/
Span<std::string> mtl_libraries() const;
private:
void add_mtl_library(StringRef path);
void add_default_mtl_library();
StringRef read_next_obj_line(StringRef &buffer);
void parse_string_buffer(StringRef &buffer_str,
Vector<std::unique_ptr<Geometry>> &r_all_geometries,
GlobalVertices &r_global_vertices,
Geometry *&curr_geom);
};
class MTLParser {
private:
char mtl_file_path_[FILE_MAX];
/**
* Directory in which the MTL file is found.
*/
char mtl_dir_path_[FILE_MAX];
public:
/**
* Open material library file.
*/
MTLParser(StringRefNull mtl_library_, StringRefNull obj_filepath);
/**
* Read MTL file(s) and add MTLMaterial instances to the given Map reference.
*/
void parse_and_store(Map<std::string, std::unique_ptr<MTLMaterial>> &r_materials);
};
} // namespace blender::io::obj

View File

@@ -0,0 +1,500 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include <algorithm>
#include "DNA_customdata_types.h"
#include "DNA_material_types.h"
#include "DNA_meshdata_types.h"
#include "BKE_attribute.h"
#include "BKE_attribute.hh"
#include "BKE_deform.hh"
#include "BKE_lib_id.hh"
#include "BKE_material.hh"
#include "BKE_mesh.hh"
#include "BKE_node_tree_update.hh"
#include "BKE_object.hh"
#include "BKE_object_deform.h"
#include "BLI_math_vector.h"
#include "BLI_set.hh"
#include "IO_validate.hh"
#include "IO_wavefront_obj.hh"
#include "importer_mesh_utils.hh"
#include "obj_export_mtl.hh"
#include "obj_import_mesh.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.obj"};
namespace io::obj {
Mesh *MeshFromGeometry::create_mesh(const OBJImportParams &import_params)
{
const int64_t tot_verts_object{mesh_geometry_.get_vertex_count()};
if (tot_verts_object <= 0) {
/* Empty mesh */
return nullptr;
}
this->fixup_invalid_faces();
if (!validate::size_fits_in_int(tot_verts_object) ||
!validate::size_fits_in_int(mesh_geometry_.edges_.size()) ||
!validate::size_fits_in_int(mesh_geometry_.face_elements_.size()) ||
!validate::size_fits_in_int(mesh_geometry_.total_corner_))
{
CLOG_WARN(&LOG, "OBJ mesh too large to import, exceeds max int size");
return nullptr;
}
/* Includes explicitly imported edges, not the ones belonging the faces to be created. */
Mesh *mesh = BKE_mesh_new_nomain(tot_verts_object,
mesh_geometry_.edges_.size(),
mesh_geometry_.face_elements_.size(),
mesh_geometry_.total_corner_);
this->create_vertices(mesh);
this->create_faces(mesh, import_params.import_vertex_groups && !import_params.use_split_groups);
this->create_edges(mesh);
this->create_uv_verts(mesh);
this->create_normals(mesh);
this->create_colors(mesh);
if (import_params.validate_meshes || mesh_geometry_.has_invalid_faces_) {
bool verbose_validate = false;
#ifndef NDEBUG
verbose_validate = true;
#endif
bke::mesh_validate(*mesh, verbose_validate);
}
return mesh;
}
Object *MeshFromGeometry::create_mesh_object(
Main *bmain,
Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
Map<std::string, Material *> &created_materials,
const OBJImportParams &import_params)
{
Mesh *mesh = this->create_mesh(import_params);
if (mesh == nullptr) {
return nullptr;
}
std::string ob_name = get_geometry_name(mesh_geometry_.geometry_name_,
import_params.collection_separator);
if (ob_name.empty()) {
ob_name = "Untitled";
}
Object *obj = BKE_object_add_only_object(bmain, OB_MESH, ob_name.c_str());
obj->data = static_cast<ID *>(BKE_object_obdata_add_from_type(bmain, OB_MESH, ob_name.c_str()));
this->create_materials(bmain,
materials,
created_materials,
obj,
import_params.relative_paths,
import_params.mtl_name_collision_mode);
BKE_mesh_nomain_to_mesh(mesh, id_cast<Mesh *>(obj->data), obj);
transform_object(obj, import_params);
/* NOTE: vertex groups have to be created after final mesh is assigned to the object. */
this->create_vertex_groups(obj);
return obj;
}
void MeshFromGeometry::fixup_invalid_faces()
{
for (int64_t face_idx = 0; face_idx < mesh_geometry_.face_elements_.size(); ++face_idx) {
const FaceElem &curr_face = mesh_geometry_.face_elements_[face_idx];
if (curr_face.corner_count_ < 3) {
/* Skip and remove faces that have fewer than 3 corners. */
mesh_geometry_.total_corner_ -= curr_face.corner_count_;
mesh_geometry_.face_elements_.remove_and_reorder(face_idx);
--face_idx;
continue;
}
/* Check if face is invalid for Blender conventions:
* basically whether it has duplicate vertex indices. */
bool valid = true;
Set<int, 8> used_verts;
for (const int64_t i : IndexRange(curr_face.corner_count_)) {
const int64_t corner_idx = curr_face.start_index_ + i;
const int vertex_idx = mesh_geometry_.face_corners_[corner_idx].vert_index;
if (used_verts.contains(vertex_idx)) {
valid = false;
break;
}
used_verts.add(vertex_idx);
}
if (valid) {
continue;
}
/* We have an invalid face, have to turn it into possibly
* multiple valid faces. */
Vector<int, 8> face_verts;
Vector<int, 8> face_uvs;
Vector<int, 8> face_normals;
face_verts.reserve(curr_face.corner_count_);
face_uvs.reserve(curr_face.corner_count_);
face_normals.reserve(curr_face.corner_count_);
for (const int64_t i : IndexRange(curr_face.corner_count_)) {
const int64_t corner_idx = curr_face.start_index_ + i;
const FaceCorner &corner = mesh_geometry_.face_corners_[corner_idx];
face_verts.append(corner.vert_index);
face_normals.append(corner.vertex_normal_index);
face_uvs.append(corner.uv_vert_index);
}
int face_vertex_group = curr_face.vertex_group_index;
int face_material = curr_face.material_index;
bool face_shaded_smooth = curr_face.shaded_smooth;
/* Remove the invalid face. */
mesh_geometry_.total_corner_ -= curr_face.corner_count_;
mesh_geometry_.face_elements_.remove_and_reorder(face_idx);
--face_idx;
Vector<Vector<int>> new_faces = fixup_invalid_face(global_vertices_.vertices, face_verts);
/* Create the newly formed faces. */
for (Span<int> face : new_faces) {
if (face.size() < 3) {
continue;
}
FaceElem new_face{};
new_face.vertex_group_index = face_vertex_group;
new_face.material_index = face_material;
new_face.shaded_smooth = face_shaded_smooth;
new_face.start_index_ = mesh_geometry_.face_corners_.size();
new_face.corner_count_ = face.size();
for (int idx : face) {
BLI_assert(idx >= 0 && idx < face_verts.size());
mesh_geometry_.face_corners_.append({face_verts[idx], face_uvs[idx], face_normals[idx]});
}
mesh_geometry_.face_elements_.append(new_face);
mesh_geometry_.total_corner_ += face.size();
}
}
}
void MeshFromGeometry::create_vertices(Mesh *mesh)
{
MutableSpan<float3> positions = mesh->vert_positions_for_write();
/* Go through all the global vertex indices from min to max,
* checking which ones are actually and building a global->local
* index mapping. Write out the used vertex positions into the Mesh
* data. */
mesh_geometry_.global_to_local_vertices_.clear();
mesh_geometry_.global_to_local_vertices_.reserve(mesh_geometry_.vertices_.size());
for (int vi = mesh_geometry_.vertex_index_min_; vi <= mesh_geometry_.vertex_index_max_; ++vi) {
BLI_assert(vi >= 0 && vi < global_vertices_.vertices.size());
if (!mesh_geometry_.vertices_.contains(vi)) {
continue;
}
int local_vi = int(mesh_geometry_.global_to_local_vertices_.size());
BLI_assert(local_vi >= 0 && local_vi < mesh->verts_num);
copy_v3_v3(positions[local_vi], global_vertices_.vertices[vi]);
mesh_geometry_.global_to_local_vertices_.add_new(vi, local_vi);
}
}
void MeshFromGeometry::create_faces(Mesh *mesh, bool use_vertex_groups)
{
MutableSpan<MDeformVert> dverts;
const int64_t total_verts = mesh_geometry_.get_vertex_count();
if (use_vertex_groups && total_verts && mesh_geometry_.has_vertex_groups_) {
dverts = mesh->deform_verts_for_write();
}
Span<float3> positions = mesh->vert_positions();
MutableSpan<int> face_offsets = mesh->face_offsets_for_write();
MutableSpan<int> corner_verts = mesh->corner_verts_for_write();
bke::MutableAttributeAccessor attributes = mesh->attributes_for_write();
bke::SpanAttributeWriter<int> material_indices =
attributes.lookup_or_add_for_write_only_span<int>("material_index", bke::AttrDomain::Face);
const bool set_face_sharpness = !has_normals();
bke::SpanAttributeWriter<bool> sharp_faces = attributes.lookup_or_add_for_write_span<bool>(
"sharp_face", bke::AttrDomain::Face);
int corner_index = 0;
for (int face_idx = 0; face_idx < mesh->faces_num; ++face_idx) {
const FaceElem &curr_face = mesh_geometry_.face_elements_[face_idx];
if (curr_face.corner_count_ < 3) {
/* Don't add single vertex face, or edges. */
CLOG_WARN(&LOG, "Face with less than 3 vertices found, skipping.");
continue;
}
face_offsets[face_idx] = corner_index;
if (set_face_sharpness) {
/* If we have no vertex normals, set face sharpness flag based on
* whether smooth shading is off. */
sharp_faces.span[face_idx] = !curr_face.shaded_smooth;
}
material_indices.span[face_idx] = curr_face.material_index;
/* Importing obj files without any materials would result in negative indices, which is not
* supported. */
material_indices.span[face_idx] = std::max(material_indices.span[face_idx], 0);
for (int64_t idx = 0; idx < curr_face.corner_count_; ++idx) {
const FaceCorner &curr_corner = mesh_geometry_.face_corners_[curr_face.start_index_ + idx];
corner_verts[corner_index] = mesh_geometry_.global_to_local_vertices_.lookup_default(
curr_corner.vert_index, 0);
/* Setup vertex group data, if needed. */
if (!dverts.is_empty()) {
const int group_index = curr_face.vertex_group_index;
/* NOTE: face might not belong to any group. */
if (group_index >= 0) {
MDeformWeight *dw = BKE_defvert_ensure_index(&dverts[corner_verts[corner_index]],
group_index);
dw->weight = 1.0f;
}
}
corner_index++;
}
if (!set_face_sharpness) {
/* If we do have vertex normals, we do not want to set face sharpness.
* Exception is, if degenerate faces (zero area, with co-colocated
* vertices) are present in the input data; this confuses custom
* corner normals calculation in Blender. Set such faces as sharp,
* they will be not shared across smooth vertex face fans. */
const float area = bke::mesh::face_area_calc(
positions, corner_verts.slice(face_offsets[face_idx], curr_face.corner_count_));
if (area < 1.0e-12f) {
sharp_faces.span[face_idx] = true;
}
}
}
material_indices.finish();
sharp_faces.finish();
}
void MeshFromGeometry::create_vertex_groups(Object *obj)
{
Mesh *mesh = id_cast<Mesh *>(obj->data);
if (mesh->deform_verts().is_empty()) {
return;
}
for (const std::string &name : mesh_geometry_.group_order_) {
BKE_object_defgroup_add_name(obj, name.data());
}
}
void MeshFromGeometry::create_edges(Mesh *mesh)
{
MutableSpan<int2> edges = mesh->edges_for_write();
const int64_t tot_edges{mesh_geometry_.edges_.size()};
const int64_t total_verts{mesh_geometry_.get_vertex_count()};
UNUSED_VARS_NDEBUG(total_verts);
for (int i = 0; i < tot_edges; ++i) {
const int2 &src_edge = mesh_geometry_.edges_[i];
int2 &dst_edge = edges[i];
dst_edge[0] = mesh_geometry_.global_to_local_vertices_.lookup_default(src_edge[0], 0);
dst_edge[1] = mesh_geometry_.global_to_local_vertices_.lookup_default(src_edge[1], 0);
BLI_assert(dst_edge[0] < total_verts && dst_edge[1] < total_verts);
}
/* Set argument `update` to true so that existing, explicitly imported edges can be merged
* with the new ones created from faces. */
bke::mesh_calc_edges(*mesh, true, false);
}
void MeshFromGeometry::create_uv_verts(Mesh *mesh)
{
if (global_vertices_.uv_vertices.size() <= 0) {
return;
}
bke::MutableAttributeAccessor attributes = mesh->attributes_for_write();
bke::SpanAttributeWriter<float2> uv_map = attributes.lookup_or_add_for_write_only_span<float2>(
"UVMap", bke::AttrDomain::Corner);
int corner_index = 0;
bool added_uv = false;
for (const FaceElem &curr_face : mesh_geometry_.face_elements_) {
for (int64_t idx = 0; idx < curr_face.corner_count_; ++idx) {
const FaceCorner &curr_corner = mesh_geometry_.face_corners_[curr_face.start_index_ + idx];
if (curr_corner.uv_vert_index >= 0 &&
curr_corner.uv_vert_index < global_vertices_.uv_vertices.size())
{
uv_map.span[corner_index] = global_vertices_.uv_vertices[curr_corner.uv_vert_index];
added_uv = true;
}
else {
uv_map.span[corner_index] = {0.0f, 0.0f};
}
corner_index++;
}
}
uv_map.finish();
/* If we have an object without UVs which resides in the same `.obj` file
* as an object which *does* have UVs we can end up adding a UV layer
* filled with zeroes.
* We could maybe check before creating this layer but that would need
* iterating over the whole mesh to check for UVs and as this is probably
* the exception rather than the rule, just delete it afterwards.
*/
if (!added_uv) {
attributes.remove("UVMap");
}
else {
mesh->uv_maps_active_set("UVMap");
mesh->uv_maps_default_set("UVMap");
}
}
static Material *get_or_create_material(Main *bmain,
const std::string &name,
Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
Map<std::string, Material *> &created_materials,
bool relative_paths,
eOBJMtlNameCollisionMode mtl_name_collision_mode)
{
/* Have we created this material already in this import session? */
Material **found_mat = created_materials.lookup_ptr(name);
if (found_mat != nullptr) {
return *found_mat;
}
/* Check if a material with this name already exists in the main database */
Material *existing_mat = id_cast<Material *>(BKE_libblock_find_name(bmain, ID_MA, name.c_str()));
if (existing_mat != nullptr &&
mtl_name_collision_mode == OBJ_MTL_NAME_COLLISION_REFERENCE_EXISTING)
{
/* If the collision mode is set to reference existing materials, use the existing one */
created_materials.add_new(name, existing_mat);
return existing_mat;
}
/* We need to create a new material */
const MTLMaterial &mtl = *materials.lookup_or_add(name, std::make_unique<MTLMaterial>());
/* If we're in MAKE_UNIQUE mode and a material with this name already exists,
* BKE_material_add will automatically create a unique name */
Material *mat = BKE_material_add(bmain, name.c_str());
id_us_min(&mat->id);
mat->nodetree = create_mtl_node_tree(bmain, mtl, mat, relative_paths);
BKE_ntree_update_after_single_tree_change(*bmain, *mat->nodetree);
created_materials.add_new(name, mat);
return mat;
}
void MeshFromGeometry::create_materials(Main *bmain,
Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
Map<std::string, Material *> &created_materials,
Object *obj,
bool relative_paths,
eOBJMtlNameCollisionMode mtl_name_collision_mode)
{
for (const std::string &name : mesh_geometry_.material_order_) {
Material *mat = get_or_create_material(
bmain, name, materials, created_materials, relative_paths, mtl_name_collision_mode);
if (mat == nullptr) {
continue;
}
BKE_object_material_assign_single_obdata(bmain, obj, mat, obj->totcol + 1);
}
if (obj->totcol > 0) {
obj->actcol = 1;
}
}
bool MeshFromGeometry::has_normals() const
{
return !global_vertices_.vert_normals.is_empty() && mesh_geometry_.total_corner_ != 0;
}
void MeshFromGeometry::create_normals(Mesh *mesh)
{
if (!has_normals()) {
return;
}
Array<float3> corner_normals(mesh_geometry_.total_corner_);
int corner_index = 0;
for (const FaceElem &curr_face : mesh_geometry_.face_elements_) {
for (int64_t idx = 0; idx < curr_face.corner_count_; ++idx) {
const FaceCorner &curr_corner = mesh_geometry_.face_corners_[curr_face.start_index_ + idx];
int n_index = curr_corner.vertex_normal_index;
float3 normal(0, 0, 0);
if (n_index >= 0 && n_index < global_vertices_.vert_normals.size()) {
normal = global_vertices_.vert_normals[n_index];
}
corner_normals[corner_index] = normal;
corner_index++;
}
}
bke::mesh_set_custom_normals(*mesh, corner_normals);
}
void MeshFromGeometry::create_colors(Mesh *mesh)
{
/* Nothing to do if we don't have vertex colors at all. */
if (global_vertices_.vertex_colors.is_empty()) {
return;
}
/* First pass to determine if we need to create a color attribute. */
for (int vi : mesh_geometry_.vertices_) {
if (!global_vertices_.has_vertex_color(vi)) {
return;
}
}
AttributeOwner owner = AttributeOwner::from_id(&mesh->id);
const std::string name = BKE_attribute_calc_unique_name(owner, "Color");
bke::MutableAttributeAccessor attributes = mesh->attributes_for_write();
bke::SpanAttributeWriter attr = attributes.lookup_or_add_for_write_span<ColorGeometry4f>(
name, bke::AttrDomain::Point);
BKE_id_attributes_active_color_set(&mesh->id, name);
BKE_id_attributes_default_color_set(&mesh->id, name);
MutableSpan<float4> colors = attr.span.cast<float4>();
/* Second pass to fill out the data. */
for (auto item : mesh_geometry_.global_to_local_vertices_.items()) {
const int vi = item.key;
const int local_vi = item.value;
BLI_assert(vi >= 0 && vi < global_vertices_.vertex_colors.size());
BLI_assert(local_vi >= 0 && local_vi < mesh->verts_num);
const float3 &c = global_vertices_.vertex_colors[vi];
colors[local_vi] = float4(c.x, c.y, c.z, 1.0);
}
attr.finish();
}
} // namespace io::obj
} // namespace blender

View File

@@ -0,0 +1,85 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "BLI_utility_mixins.hh"
#include "IO_wavefront_obj.hh"
#include "obj_import_mtl.hh"
#include "obj_import_objects.hh"
namespace blender {
struct Main;
struct Mesh;
struct Material;
struct Object;
struct OBJImportParams;
namespace io::obj {
/**
* Make a Blender Mesh Object from a Geometry of GEOM_MESH type.
*/
class MeshFromGeometry : NonMovable, NonCopyable {
private:
Geometry &mesh_geometry_;
const GlobalVertices &global_vertices_;
public:
MeshFromGeometry(Geometry &mesh_geometry, const GlobalVertices &global_vertices)
: mesh_geometry_(mesh_geometry), global_vertices_(global_vertices)
{
}
Mesh *create_mesh(const OBJImportParams &import_params);
Object *create_mesh_object(Main *bmain,
Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
Map<std::string, Material *> &created_materials,
const OBJImportParams &import_params);
private:
/**
* OBJ files coming from the wild might have faces that are invalid in Blender
* (mostly with duplicate vertex indices, used by some software to indicate
* faces with holes). This method tries to fix them up.
*/
void fixup_invalid_faces();
void create_vertices(Mesh *mesh);
/**
* Create faces for the Mesh, set smooth shading flags, Materials.
*/
void create_faces(Mesh *mesh, bool use_vertex_groups);
/**
* Add explicitly imported OBJ edges to the mesh.
*/
void create_edges(Mesh *mesh);
/**
* Add UV layer and vertices to the Mesh.
*/
void create_uv_verts(Mesh *mesh);
/**
* Add materials and the node-tree to the Mesh Object.
*/
void create_materials(Main *bmain,
Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
Map<std::string, Material *> &created_materials,
Object *obj,
bool relative_paths,
eOBJMtlNameCollisionMode mtl_name_collision_mode);
void create_normals(Mesh *mesh);
void create_colors(Mesh *mesh);
void create_vertex_groups(Object *obj);
bool has_normals() const;
};
} // namespace io::obj
} // namespace blender

View File

@@ -0,0 +1,458 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include "BKE_image.hh"
#include "BKE_main.hh"
#include "BKE_node.hh"
#include "BKE_node_legacy_types.hh"
#include "BLI_math_vector.h"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "DNA_material_types.h"
#include "DNA_node_types.h"
#include "NOD_shader.h"
#include "obj_export_mtl.hh"
#include "obj_import_mtl.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.obj"};
namespace io::obj {
/**
* Set the socket's (of given ID) value to the given number(s).
* Only float value(s) can be set using this method.
*/
static void set_property_of_socket(eNodeSocketDatatype property_type,
const char *socket_id,
Span<float> value,
bNode *r_node)
{
BLI_assert(r_node);
bNodeSocket *socket{bke::node_find_socket(*r_node, SOCK_IN, UString(socket_id))};
BLI_assert(socket && socket->type == property_type);
switch (property_type) {
case SOCK_FLOAT: {
BLI_assert(value.size() == 1);
static_cast<bNodeSocketValueFloat *>(socket->default_value)->value = value[0];
break;
}
case SOCK_RGBA: {
/* Alpha will be added manually. It is not read from the MTL file either. */
BLI_assert(value.size() == 3);
copy_v3_v3(static_cast<bNodeSocketValueRGBA *>(socket->default_value)->value, value.data());
static_cast<bNodeSocketValueRGBA *>(socket->default_value)->value[3] = 1.0f;
break;
}
case SOCK_VECTOR: {
BLI_assert(value.size() == 3);
copy_v4_v4(static_cast<bNodeSocketValueVector *>(socket->default_value)->value,
value.data());
break;
}
default: {
BLI_assert(0);
break;
}
}
}
static Image *load_image_at_path(Main *bmain, const std::string &path, bool relative_paths)
{
Image *image = BKE_image_load_exists(bmain, path.c_str());
if (!image) {
CLOG_WARN(&LOG, "Cannot load image file: '%s'", path.c_str());
return nullptr;
}
CLOG_INFO(&LOG, "Loaded image from: '%s'", path.c_str());
if (relative_paths) {
BLI_path_rel(image->filepath, BKE_main_blendfile_path(bmain));
BLI_path_normalize(image->filepath);
}
return image;
}
static Image *create_placeholder_image(Main *bmain, const std::string &path)
{
const float color[4] = {0, 0, 0, 1};
Image *image = BKE_image_add_generated(bmain,
1,
1,
BLI_path_basename(path.c_str()),
24,
false,
IMA_GENTYPE_BLANK,
color,
false,
false,
false);
STRNCPY(image->filepath, path.c_str());
/* Ensure that we are not marked as a generated image and clear any buffers created so far. */
image->source = IMA_SRC_FILE;
image->type = IMA_TYPE_IMAGE;
BKE_image_free_buffers(image);
return image;
}
static Image *load_texture_image(Main *bmain, const MTLTexMap &tex_map, bool relative_paths)
{
Image *image = nullptr;
/* Remove quotes. */
std::string image_path{tex_map.image_path};
auto end_pos = std::remove(image_path.begin(), image_path.end(), '"');
image_path.erase(end_pos, image_path.end());
/* First try treating texture path as relative. */
std::string tex_path{tex_map.mtl_dir_path + image_path};
image = load_image_at_path(bmain, tex_path, relative_paths);
if (image != nullptr) {
return image;
}
/* Then try using it directly as absolute path. */
image = load_image_at_path(bmain, image_path, relative_paths);
if (image != nullptr) {
return image;
}
/* Try replacing underscores with spaces. */
std::string no_underscore_path{image_path};
std::replace(no_underscore_path.begin(), no_underscore_path.end(), '_', ' ');
if (!ELEM(no_underscore_path, image_path, tex_path)) {
image = load_image_at_path(bmain, no_underscore_path, relative_paths);
if (image != nullptr) {
return image;
}
}
/* Try taking just the basename from input path. */
std::string base_path{tex_map.mtl_dir_path + BLI_path_basename(image_path.c_str())};
if (base_path != tex_path) {
image = load_image_at_path(bmain, base_path, relative_paths);
if (image != nullptr) {
return image;
}
}
image = create_placeholder_image(bmain, tex_path);
return image;
}
/* Nodes are arranged in columns by type, with manually placed x coordinates
* based on node widths. */
const float node_locx_texcoord = -880.0f;
const float node_locx_mapping = -680.0f;
const float node_locx_image = -480.0f;
const float node_locx_normalmap = -200.0f;
const float node_locx_bsdf = 0.0f;
const float node_locx_output = 280.0f;
/* Nodes are arranged in rows; one row for each image being used. */
const float node_locy_top = 300.0f;
const float node_locy_step = 300.0f;
/* Add a node of the given type at the given location. */
static bNode *add_node(bNodeTree *ntree, int type, float x, float y)
{
bNode *node = bke::node_add_static_node(nullptr, *ntree, type);
node->location[0] = x;
node->location[1] = y;
return node;
}
static void link_sockets(bNodeTree *ntree,
bNode *from_node,
const char *from_node_id,
bNode *to_node,
const char *to_node_id)
{
bNodeSocket *from_sock{bke::node_find_socket(*from_node, SOCK_OUT, UString(from_node_id))};
bNodeSocket *to_sock{bke::node_find_socket(*to_node, SOCK_IN, UString(to_node_id))};
BLI_assert(from_sock && to_sock);
bke::node_add_link(*ntree, *from_node, *from_sock, *to_node, *to_sock);
}
static void set_bsdf_socket_values(bNode *bsdf, Material *mat, const MTLMaterial &mtl_mat)
{
const int illum = mtl_mat.illum_mode;
bool do_highlight = false;
bool do_tranparency = false;
bool do_reflection = false;
bool do_glass = false;
/* See https://wikipedia.org/wiki/Wavefront_.obj_file for possible values of illum. */
switch (illum) {
case -1:
case 1:
/* Base color on, ambient on. */
break;
case 2: {
/* Highlight on. */
do_highlight = true;
break;
}
case 3: {
/* Reflection on and Ray trace on. */
do_reflection = true;
break;
}
case 4: {
/* Transparency: Glass on, Reflection: Ray trace on. */
do_glass = true;
do_reflection = true;
do_tranparency = true;
break;
}
case 5: {
/* Reflection: Fresnel on and Ray trace on. */
do_reflection = true;
break;
}
case 6: {
/* Transparency: Refraction on, Reflection: Fresnel off and Ray trace on. */
do_reflection = true;
do_tranparency = true;
break;
}
case 7: {
/* Transparency: Refraction on, Reflection: Fresnel on and Ray trace on. */
do_reflection = true;
do_tranparency = true;
break;
}
case 8: {
/* Reflection on and Ray trace off. */
do_reflection = true;
break;
}
case 9: {
/* Transparency: Glass on, Reflection: Ray trace off. */
do_glass = true;
do_reflection = false;
do_tranparency = true;
break;
}
default: {
CLOG_WARN(&LOG,
"Material illum value '%d' is not supported by the Principled BSDF shader.",
illum);
break;
}
}
/* Approximations for trying to map obj/mtl material model into
* Principled BSDF: */
/* Specular: average of Ks components. */
float specular = (mtl_mat.spec_color[0] + mtl_mat.spec_color[1] + mtl_mat.spec_color[2]) / 3;
if (specular < 0.0f) {
specular = do_highlight ? 1.0f : 0.0f;
}
/* Roughness: map 0..1000 range to 1..0 and apply non-linearity. */
float roughness;
if (mtl_mat.spec_exponent < 0.0f) {
roughness = do_highlight ? 0.0f : 1.0f;
}
else {
float clamped_ns = std::max(0.0f, std::min(1000.0f, mtl_mat.spec_exponent));
roughness = 1.0f - sqrt(clamped_ns / 1000.0f);
}
/* Metallic: average of `Ka` components. */
float metallic = (mtl_mat.ambient_color[0] + mtl_mat.ambient_color[1] +
mtl_mat.ambient_color[2]) /
3;
if (do_reflection) {
if (metallic < 0.0f) {
metallic = 1.0f;
}
}
else {
metallic = 0.0f;
}
float ior = mtl_mat.ior;
if (ior < 0) {
if (do_tranparency) {
ior = 1.0f;
}
if (do_glass) {
ior = 1.5f;
}
}
float alpha = mtl_mat.alpha;
if (do_tranparency && alpha < 0) {
alpha = 1.0f;
}
/* PBR values, when present, override the ones calculated above. */
if (mtl_mat.roughness >= 0) {
roughness = mtl_mat.roughness;
}
if (mtl_mat.metallic >= 0) {
metallic = mtl_mat.metallic;
}
float3 base_color = mtl_mat.color;
if (base_color.x >= 0 && base_color.y >= 0 && base_color.z >= 0) {
set_property_of_socket(SOCK_RGBA, "Base Color", {base_color, 3}, bsdf);
/* Viewport shading uses legacy r,g,b base color. */
mat->r = base_color.x;
mat->g = base_color.y;
mat->b = base_color.z;
}
if (mtl_mat.tex_map_of_type(MTLTexMapType::Emission).is_valid()) {
set_property_of_socket(SOCK_FLOAT, "Emission Strength", {1.0f}, bsdf);
}
float3 emission_color = mtl_mat.emission_color;
if (emission_color.x >= 0 && emission_color.y >= 0 && emission_color.z >= 0) {
float emission_strength = fmax(emission_color.x, fmax(emission_color.y, emission_color.z));
if (emission_strength > 1.0f) {
/* For colors brighter than 1.0, change color to be in 0..1 range, and set emission
* strength accordingly. */
set_property_of_socket(
SOCK_RGBA, "Emission Color", {emission_color / emission_strength, 3}, bsdf);
set_property_of_socket(SOCK_FLOAT, "Emission Strength", {emission_strength}, bsdf);
}
else {
set_property_of_socket(SOCK_RGBA, "Emission Color", {emission_color, 3}, bsdf);
set_property_of_socket(SOCK_FLOAT, "Emission Strength", {1.0f}, bsdf);
}
}
set_property_of_socket(SOCK_FLOAT, "Specular IOR Level", {specular}, bsdf);
set_property_of_socket(SOCK_FLOAT, "Roughness", {roughness}, bsdf);
mat->roughness = roughness;
set_property_of_socket(SOCK_FLOAT, "Metallic", {metallic}, bsdf);
mat->metallic = metallic;
/* Some files have `Ni 0`, ignore those values. */
if (ior > 0.0f) {
set_property_of_socket(SOCK_FLOAT, "IOR", {ior}, bsdf);
}
if (alpha != -1) {
set_property_of_socket(SOCK_FLOAT, "Alpha", {alpha}, bsdf);
}
if (do_tranparency || (alpha >= 0.0f && alpha < 1.0f)) {
mat->blend_method = MA_BM_BLEND;
mat->blend_flag |= MA_BL_HIDE_BACKFACE;
}
if (mtl_mat.sheen >= 0) {
set_property_of_socket(SOCK_FLOAT, "Sheen Weight", {mtl_mat.sheen}, bsdf);
}
if (mtl_mat.cc_thickness >= 0) {
/* Clearcoat used to include an implicit 0.25 factor, so stay compatible to old versions. */
set_property_of_socket(SOCK_FLOAT, "Coat Weight", {0.25f * mtl_mat.cc_thickness}, bsdf);
}
if (mtl_mat.cc_roughness >= 0) {
set_property_of_socket(SOCK_FLOAT, "Coat Roughness", {mtl_mat.cc_roughness}, bsdf);
}
if (mtl_mat.aniso >= 0) {
set_property_of_socket(SOCK_FLOAT, "Anisotropic", {mtl_mat.aniso}, bsdf);
}
if (mtl_mat.aniso_rot >= 0) {
set_property_of_socket(SOCK_FLOAT, "Anisotropic Rotation", {mtl_mat.aniso_rot}, bsdf);
}
/* Transmission: average of transmission color. */
float transmission = (mtl_mat.transmit_color[0] + mtl_mat.transmit_color[1] +
mtl_mat.transmit_color[2]) /
3;
if (transmission >= 0) {
set_property_of_socket(SOCK_FLOAT, "Transmission Weight", {transmission}, bsdf);
}
}
static void add_image_textures(Main *bmain,
bNodeTree *ntree,
bNode *bsdf,
Material *mat,
const MTLMaterial &mtl_mat,
bool relative_paths)
{
float node_locy = node_locy_top;
for (int key = 0; key < int(MTLTexMapType::Count); ++key) {
const MTLTexMap &value = mtl_mat.texture_maps[key];
if (!value.is_valid()) {
/* No Image texture node of this map type can be added to this material. */
continue;
}
Image *image = load_texture_image(bmain, value, relative_paths);
if (image == nullptr) {
continue;
}
bNode *image_node = add_node(ntree, SH_NODE_TEX_IMAGE, node_locx_image, node_locy);
BLI_assert(image_node);
image_node->id = &image->id;
static_cast<NodeTexImage *>(image_node->storage)->projection = value.projection_type;
/* Add normal map node if needed. */
bNode *normal_map = nullptr;
if (key == int(MTLTexMapType::Normal)) {
normal_map = add_node(ntree, SH_NODE_NORMAL_MAP, node_locx_normalmap, node_locy);
const float bump = std::max(0.0f, mtl_mat.normal_strength);
set_property_of_socket(SOCK_FLOAT, "Strength", {bump}, normal_map);
}
/* Add UV mapping & coordinate nodes only if needed. */
if (value.translation != float3(0, 0, 0) || value.scale != float3(1, 1, 1)) {
bNode *texcoord = add_node(ntree, SH_NODE_TEX_COORD, node_locx_texcoord, node_locy);
bNode *mapping = add_node(ntree, SH_NODE_MAPPING, node_locx_mapping, node_locy);
set_property_of_socket(SOCK_VECTOR, "Location", {value.translation, 3}, mapping);
set_property_of_socket(SOCK_VECTOR, "Scale", {value.scale, 3}, mapping);
link_sockets(ntree, texcoord, "UV", mapping, "Vector");
link_sockets(ntree, mapping, "Vector", image_node, "Vector");
}
if (normal_map) {
link_sockets(ntree, image_node, "Color", normal_map, "Color");
link_sockets(ntree, normal_map, "Normal", bsdf, "Normal");
}
else if (key == int(MTLTexMapType::Alpha)) {
link_sockets(ntree, image_node, "Alpha", bsdf, tex_map_type_to_socket_id[key]);
mat->blend_method = MA_BM_BLEND;
mat->blend_flag |= MA_BL_HIDE_BACKFACE;
}
else {
link_sockets(ntree, image_node, "Color", bsdf, tex_map_type_to_socket_id[key]);
}
/* Next layout row: goes downwards on the screen. */
node_locy -= node_locy_step;
}
}
bNodeTree *create_mtl_node_tree(Main *bmain,
const MTLMaterial &mtl_mat,
Material *mat,
bool relative_paths)
{
bNodeTree *ntree = mat->nodetree;
BLI_assert(mat->nodetree);
bNode *bsdf = add_node(ntree, SH_NODE_BSDF_PRINCIPLED, node_locx_bsdf, node_locy_top);
bNode *output = add_node(ntree, SH_NODE_OUTPUT_MATERIAL, node_locx_output, node_locy_top);
set_bsdf_socket_values(bsdf, mat, mtl_mat);
add_image_textures(bmain, ntree, bsdf, mat, mtl_mat, relative_paths);
link_sockets(ntree, bsdf, "BSDF", output, "Surface");
bke::node_set_active(*ntree, *output);
return ntree;
}
} // namespace io::obj
} // namespace blender

View File

@@ -0,0 +1,23 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
namespace blender {
struct bNodeTree;
struct Main;
struct Material;
namespace io::obj {
struct MTLMaterial;
bNodeTree *create_mtl_node_tree(Main *bmain,
const MTLMaterial &mtl_mat,
Material *mat,
bool relative_paths);
} // namespace io::obj
} // namespace blender

View File

@@ -0,0 +1,392 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include "BKE_curve_legacy_convert.hh"
#include "BKE_curves.hh"
#include "BKE_lib_id.hh"
#include "BKE_object.hh"
#include "BLI_array_utils.hh"
#include "BLI_listbase.h"
#include "BLI_math_vector.h"
#include "DNA_curve_types.h"
#include "IO_wavefront_obj.hh"
#include "importer_mesh_utils.hh"
#include "obj_import_nurbs.hh"
#include "obj_import_objects.hh"
namespace blender::io::obj {
Curves *io::obj::CurveFromGeometry::create_curve(const OBJImportParams &import_params)
{
BLI_assert(!curve_geometry_.nurbs_element_.curv_indices.is_empty());
Curves *curves_id = bke::curves_new_nomain(0, 0);
bke::CurvesGeometry &curves = curves_id->geometry.wrap();
this->create_nurbs(curves, import_params);
return curves_id;
}
Object *CurveFromGeometry::create_curve_object(Main *bmain, const OBJImportParams &import_params)
{
if (curve_geometry_.nurbs_element_.curv_indices.is_empty()) {
return nullptr;
}
std::string ob_name = get_geometry_name(curve_geometry_.geometry_name_,
import_params.collection_separator);
if (ob_name.empty() && !curve_geometry_.nurbs_element_.group_.empty()) {
ob_name = curve_geometry_.nurbs_element_.group_;
}
if (ob_name.empty()) {
ob_name = "Untitled";
}
Curve *curve = BKE_curve_add(bmain, ob_name.c_str(), OB_CURVES_LEGACY);
Object *obj = BKE_object_add_only_object(bmain, OB_CURVES_LEGACY, ob_name.c_str());
curve->flag = CU_3D;
curve->resolu = curve->resolv = 12;
/* Only one NURBS spline will be created in the curve object. */
curve->actnu = 0;
Nurb *nurb = MEM_new<Nurb>(__func__);
BLI_addtail(BKE_curve_nurbs_get(curve), nurb);
this->create_nurbs(curve, import_params);
obj->data = id_cast<ID *>(curve);
transform_object(obj, import_params);
return obj;
}
static int8_t get_valid_nurbs_degree(const NurbsElement &element)
{
/* Use max(1, min()) to avoid undefined clamp behavior when curve_indices.size() == 0 */
const int degree = std::max(1, std::min<int>(element.degree, element.curv_indices.size() - 1));
return degree + 1 > std::numeric_limits<int8_t>::max() ? std::numeric_limits<int8_t>::max() - 1 :
int8_t(degree);
}
/**
* Get the number of control points repeated for a cyclic curve given the multiplicity found
* at the endpoints (assumes cyclic curve).
*/
static int repeating_cyclic_point_num(const int8_t order, const Span<float> knots)
{
/* Due to the additional start knot, drop first.
*/
Vector<int> multiplicity = bke::curves::nurbs::calculate_multiplicity_sequence(
knots.slice(1, order - 1));
BLI_assert(order > multiplicity.first());
return order - multiplicity.first();
}
void CurveFromGeometry::create_nurbs(Curve *curve, const OBJImportParams &import_params)
{
const NurbsElement &nurbs_geometry = curve_geometry_.nurbs_element_;
const int8_t degree = get_valid_nurbs_degree(nurbs_geometry);
Nurb *nurb = static_cast<Nurb *>(curve->nurb.first);
nurb->type = CU_NURBS;
nurb->flag = CU_SMOOTH;
nurb->next = nurb->prev = nullptr;
/* BKE_nurb_points_add later on will update pntsu. If this were set to total curve points,
* we get double the total points in viewport. */
nurb->pntsu = 0;
/* Total points = pntsu * pntsv. */
nurb->pntsv = 1;
nurb->orderu = nurb->orderv = degree + 1;
nurb->resolu = nurb->resolv = curve->resolu;
const Vector<int> multiplicity = bke::curves::nurbs::calculate_multiplicity_sequence(
nurbs_geometry.parm);
nurb->flagu = this->detect_knot_mode(
import_params, degree, nurbs_geometry.curv_indices, nurbs_geometry.parm, multiplicity);
const int repeated_points = nurb->flagu & CU_NURB_CYCLIC ?
repeating_cyclic_point_num(nurb->orderu, nurbs_geometry.parm) :
0;
const Span<int> indices = nurbs_geometry.curv_indices.as_span().slice(
nurbs_geometry.curv_indices.index_range().drop_back(repeated_points));
BKE_nurb_points_add(nurb, indices.size());
for (const int i : indices.index_range()) {
BPoint &bpoint = nurb->bp[i];
copy_v3_v3(bpoint.vec, global_vertices_.vertices[indices[i]]);
bpoint.vec[3] = (global_vertices_.vertex_weights.size() > indices[i]) ?
global_vertices_.vertex_weights[indices[i]] :
1.0f;
bpoint.weight = 1.0f;
}
if (nurb->flagu & CU_NURB_CUSTOM) {
BKE_nurb_knot_alloc_u(nurb);
MutableSpan<float> knots_dst_u{nurb->knotsu, KNOTSU(nurb)};
array_utils::copy<float>(nurbs_geometry.parm, knots_dst_u);
}
else {
BKE_nurb_knot_calc_u(nurb);
}
}
void CurveFromGeometry::create_nurbs(bke::CurvesGeometry &curves,
const OBJImportParams &import_params)
{
const NurbsElement &nurbs_geometry = curve_geometry_.nurbs_element_;
const int8_t degree = get_valid_nurbs_degree(nurbs_geometry);
const int8_t order = degree + 1;
const Vector<int> multiplicity = bke::curves::nurbs::calculate_multiplicity_sequence(
nurbs_geometry.parm);
const short knot_flag = this->detect_knot_mode(
import_params, degree, nurbs_geometry.curv_indices, nurbs_geometry.parm, multiplicity);
const bool is_cyclic = knot_flag & CU_NURB_CYCLIC;
const int repeated_points = is_cyclic ? repeating_cyclic_point_num(order, nurbs_geometry.parm) :
0;
const Span<int> indices = nurbs_geometry.curv_indices.as_span().slice(
nurbs_geometry.curv_indices.index_range().drop_back(repeated_points));
const int points_num = indices.size();
const int curve_index = 0;
curves.resize(points_num, 1);
MutableSpan<int8_t> types = curves.curve_types_for_write();
MutableSpan<bool> cyclic = curves.cyclic_for_write();
MutableSpan<int8_t> orders = curves.nurbs_orders_for_write();
MutableSpan<int8_t> modes = curves.nurbs_knots_modes_for_write();
types.first() = CURVE_TYPE_NURBS;
cyclic.first() = is_cyclic;
orders.first() = order;
modes.first() = bke::knots_mode_from_legacy(knot_flag);
curves.update_curve_types();
const OffsetIndices points_by_curve = curves.points_by_curve();
const IndexRange point_range = points_by_curve[curve_index];
MutableSpan<float3> positions = curves.positions_for_write().slice(point_range);
MutableSpan<float> weights = curves.nurbs_weights_for_write().slice(point_range);
for (const int i : indices.index_range()) {
positions[i] = global_vertices_.vertices[indices[i]];
weights[i] = (global_vertices_.vertex_weights.size() > indices[i]) ?
global_vertices_.vertex_weights[indices[i]] :
1.0f;
}
if (modes.first() == NURBS_KNOT_MODE_CUSTOM) {
OffsetIndices<int> knot_offsets = curves.nurbs_custom_knots_by_curve();
curves.nurbs_custom_knots_update_size();
MutableSpan<float> knots = curves.nurbs_custom_knots_for_write().slice(
knot_offsets[curve_index]);
array_utils::copy<float>(nurbs_geometry.parm, knots);
}
}
static bool detect_clamped_endpoint(const int8_t degree, const Span<int> multiplicity)
{
const int8_t order = degree + 1;
/* Consider any combination of following patterns as clamped:
*
* O ..
* 1 d ..
*/
const bool begin_clamped = multiplicity.first() == order ||
(multiplicity.first() == 1 && multiplicity[1] == degree);
const bool end_clamped = multiplicity.last() == order ||
(multiplicity.last() == 1 && multiplicity.last(1) == degree);
return begin_clamped && end_clamped;
}
static bool almost_equal_relative(const float a, const float b, const float epsilon)
{
const float abs_diff = std::abs(b - a);
return abs_diff < a * epsilon;
}
static bool detect_knot_mode_cyclic(const int8_t degree,
const Span<int> indices,
const Span<float> knots,
const Span<int> multiplicity,
const bool is_clamped)
{
constexpr float epsilon = 1e-4;
const int8_t order = degree + 1;
const int repeated_points = repeating_cyclic_point_num(order, knots);
BLI_assert(repeated_points > 0);
const Span<int> indices_tail = indices.take_back(repeated_points);
for (const int64_t i : indices_tail.index_range()) {
if (indices[i] != indices_tail[i]) {
return false;
}
}
/* Multiplicity m is continuous to the `degree - m` derivative and as such
* `multiplicity == degree` is discontinuous. Due to the superfluous knots
* the first/last entry can be up to `order`, remaining up to `degree`.
*/
if (multiplicity.first() > order || multiplicity.last() > order) {
return false;
}
for (const int m : multiplicity.drop_front(1).drop_back(1)) {
if (m > degree) {
return false;
}
}
if (is_clamped) {
/* Clamped curves are discontinuous at the ends and have no overlapping spans. */
return true;
}
/* Ensure it matches on both of the knot spans adjacent to the start/end of the parameter range.
*/
const Span<float> knots_tail = knots.take_back(2 * degree + 1);
for (const int64_t i : knots_tail.index_range().drop_back(1)) {
const float head_span = knots[i + 1] - knots[i];
const float tail_span = knots_tail[i + 1] - knots_tail[i];
if (!almost_equal_relative(head_span, tail_span, epsilon)) {
return false;
}
}
return true;
}
static bool detect_knot_mode_bezier_clamped(const int8_t degree,
const int num_points,
const Span<int> multiplicity)
{
const int8_t order = degree + 1;
/* Don't treat polylines as Beziers. */
if (order == 2) {
return false;
}
/* Allow patterns:
* `O d` ..
* `1 d d` ..
*/
if (multiplicity[0] < order && (multiplicity[0] != 1 || multiplicity[1] < degree)) {
return false;
}
Span<int> mdegree_span = multiplicity.drop_front(1);
if (multiplicity.size() == 2) {
/* Single segment, allow patterns:
* `O a`
* where `a > 0`
*/
if (multiplicity.first() != order) {
return false;
}
}
else {
/* Allow patterns:
* .. `d O+`
* .. `d d 1`
*/
if (multiplicity.last() != order &&
(multiplicity.last() == 1 && multiplicity.last(1) != degree))
{
/* No match to the valid patterns. */
return false;
}
const int remainder = (num_points - 1) % degree;
if (multiplicity.last() != order + remainder &&
(multiplicity.last() != 1 || multiplicity.last(1) < degree))
{
return false;
}
}
mdegree_span = mdegree_span.drop_back(1);
/* Verify all other knots are of degree multiplicity */
for (const int m : mdegree_span) {
if (m != degree) {
return false;
}
}
return true;
}
static bool detect_knot_mode_uniform(const int8_t degree,
const Span<float> knots,
const Span<int> multiplicity,
const bool clamped)
{
constexpr float epsilon = 1e-4;
/* Check if knot count matches multiplicity adjusted for clamped ends. For a uniform non-clamped
* curve, all multiplicity entries equals 1 and the array size should match.
*/
const int O1_clamps = int(multiplicity.first() == 1) + int(multiplicity.last() == 1);
const int clamped_offset = clamped ? 2 * degree - O1_clamps : 0;
if (knots.size() != multiplicity.size() + clamped_offset) {
return false;
}
/* Ensure it's not a single segment with clamped ends (it would be a Bezier segment). */
const Span<float> unclamped_knots = knots.drop_front(clamped_offset).drop_back(clamped_offset);
if (unclamped_knots.size() == 2) {
return false;
}
if (unclamped_knots.size() < 2) {
/* Classify single point as uniform? */
return true;
}
/* Verify spacing is uniform (excluding clamped ends). */
const float uniform_delta = unclamped_knots[1] - unclamped_knots[0];
for (const int64_t i : unclamped_knots.index_range().drop_front(2)) {
const float delta = unclamped_knots[i] - unclamped_knots[i - 1];
if (!almost_equal_relative(delta, uniform_delta, epsilon)) {
return false;
}
}
return true;
}
eNurbKnotFlag CurveFromGeometry::detect_knot_mode(const OBJImportParams &import_params,
const int8_t degree,
const Span<int> indices,
const Span<float> knots,
const Span<int> multiplicity)
{
eNurbKnotFlag knot_mode = {};
const bool is_clamped = detect_clamped_endpoint(degree, multiplicity);
const bool is_bezier = detect_knot_mode_bezier_clamped(degree, indices.size(), multiplicity);
if (is_bezier) {
SET_FLAG_FROM_TEST(knot_mode, true, CU_NURB_ENDPOINT);
SET_FLAG_FROM_TEST(knot_mode, true, CU_NURB_BEZIER);
}
else {
const bool is_uniform = detect_knot_mode_uniform(degree, knots, multiplicity, is_clamped);
SET_FLAG_FROM_TEST(knot_mode, is_clamped, CU_NURB_ENDPOINT);
SET_FLAG_FROM_TEST(knot_mode, !is_uniform, CU_NURB_CUSTOM);
}
const bool check_cyclic = import_params.close_spline_loops && indices.size() > degree;
const bool no_custom_cyclic = knot_mode & CU_NURB_CUSTOM;
if (check_cyclic && !no_custom_cyclic) {
const bool is_cyclic = detect_knot_mode_cyclic(
degree, indices, knots, multiplicity, is_clamped);
SET_FLAG_FROM_TEST(knot_mode, is_cyclic, CU_NURB_CYCLIC);
}
return knot_mode;
}
} // namespace blender::io::obj

View File

@@ -0,0 +1,60 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "BKE_curve.hh"
#include "BLI_utility_mixins.hh"
#include "DNA_curve_types.h"
#include "obj_import_objects.hh"
namespace blender {
struct OBJImportParams;
namespace bke {
class CurvesGeometry;
};
namespace io::obj {
/**
* Make a Blender NURBS Curve block from a Geometry of GEOM_CURVE type.
*/
class CurveFromGeometry : NonMovable, NonCopyable {
private:
const Geometry &curve_geometry_;
const GlobalVertices &global_vertices_;
public:
CurveFromGeometry(const Geometry &geometry, const GlobalVertices &global_vertices)
: curve_geometry_(geometry), global_vertices_(global_vertices)
{
}
Curves *create_curve(const OBJImportParams &import_params);
Object *create_curve_object(Main *bmain, const OBJImportParams &import_params);
private:
/**
* Create a NURBS spline for the Curve converted from Geometry.
*/
void create_nurbs(Curve *curve, const OBJImportParams &import_params);
void create_nurbs(bke::CurvesGeometry &curve, const OBJImportParams &import_params);
eNurbKnotFlag detect_knot_mode(const OBJImportParams &import_params,
int8_t degree,
Span<int> indices,
Span<float> knots,
Span<int> multiplicity);
};
} // namespace io::obj
} // namespace blender

View File

@@ -0,0 +1,178 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "BLI_map.hh"
#include "BLI_math_base.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_set.hh"
#include "BLI_vector.hh"
#include "DNA_object_types.h"
namespace blender::io::obj {
/**
* All vertex positions, normals, UVs, colors in the OBJ file.
*/
struct GlobalVertices {
Vector<float3> vertices;
Vector<float2> uv_vertices;
Vector<float3> vert_normals;
/**
* Vertex color for each vertex. -1 indicates no vertex color was specified.
* Being shorter than vertices also means the missing vertices had no color.
*/
Vector<float3> vertex_colors;
/**
* Vertex weight for each vertex.
* Being shorter than vertices also means the missing vertices had no weight.
*/
Vector<float> vertex_weights;
/**
* Block of colors buffered for #MRGB extension.
* Flushed to vertex_colors when complete (at next vertex or end-of-file).
*/
Vector<float3> mrgb_block;
void set_vertex_color(int64_t index, float3 color)
{
if (index >= vertex_colors.size()) {
vertex_colors.resize(index + 1, float3(-1.0, -1.0, -1.0));
}
vertex_colors[index] = color;
}
void set_vertex_weight(int64_t index, float weight)
{
if (index >= vertex_weights.size()) {
vertex_weights.resize(index + 1, 1.0);
}
vertex_weights[index] = weight;
}
bool has_vertex_color(int64_t index) const
{
return index < vertex_colors.size() && vertex_colors[index].x >= 0.0;
}
void flush_mrgb_block()
{
if (!mrgb_block.is_empty()) {
/* Set color of the last mrgb_block.size() verts. */
int64_t start_of_block = 0;
if (mrgb_block.size() <= vertices.size()) {
start_of_block = vertices.size() - mrgb_block.size();
}
if (start_of_block == 0) {
vertex_colors = std::move(mrgb_block);
}
else {
vertex_colors.resize(start_of_block, float3(-1.0, -1.0, -1.0));
vertex_colors.extend(mrgb_block);
}
mrgb_block.clear();
}
}
};
/**
* A face's corner in an OBJ file. In Blender, it translates to a corner vertex.
*/
struct FaceCorner {
/* These indices range from zero to total vertices in the OBJ file. */
int vert_index;
/* -1 is to indicate absence of UV vertices. Only < 0 condition should be checked since
* it can be less than -1 too. */
int uv_vert_index = -1;
int vertex_normal_index = -1;
};
struct FaceElem {
int vertex_group_index = -1;
int material_index = -1;
bool shaded_smooth = false;
int64_t start_index_ = 0;
int64_t corner_count_ = 0;
};
/**
* Contains data for one single NURBS curve in the OBJ file.
*/
struct NurbsElement {
/**
* For curves, groups may be used to specify multiple splines in the same curve object.
* It may also serve as the name of the curve if not specified explicitly.
*/
std::string group_;
int degree = 0;
float2 range{0.0f, 1.0f};
/**
* Indices into the global list of vertex coordinates. Must be non-negative.
*/
Vector<int> curv_indices;
/* Values in the parm u/v line in a curve definition. */
Vector<float> parm;
};
enum eGeometryType {
GEOM_MESH = OB_MESH,
GEOM_CURVE = OB_CURVES_LEGACY,
};
struct Geometry {
eGeometryType geom_type_ = GEOM_MESH;
std::string geometry_name_;
Map<std::string, int> group_indices_;
Vector<std::string> group_order_;
Map<std::string, int> material_indices_;
Vector<std::string> material_order_;
int vertex_index_min_ = INT_MAX;
int vertex_index_max_ = -1;
/* Global vertex indices used by this geometry. */
Set<int> vertices_;
/* Mapping from global vertex index to geometry-local vertex index. */
Map<int, int> global_to_local_vertices_;
/* Loose edges in the file. */
Vector<int2> edges_;
Vector<FaceCorner> face_corners_;
Vector<FaceElem> face_elements_;
bool has_invalid_faces_ = false;
bool has_vertex_groups_ = false;
NurbsElement nurbs_element_;
int64_t total_corner_ = 0;
int get_vertex_count() const
{
return int(vertices_.size());
}
void track_vertex_index(int index)
{
vertices_.add(index);
math::min_inplace(vertex_index_min_, index);
math::max_inplace(vertex_index_max_, index);
}
void track_all_vertices(int count)
{
vertices_.reserve(count);
for (int i = 0; i < count; ++i) {
vertices_.add(i);
}
vertex_index_min_ = 0;
vertex_index_max_ = count - 1;
}
};
} // namespace blender::io::obj

View File

@@ -0,0 +1,268 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#include <string>
#include "BLI_bounds.hh"
#include "BLI_listbase.h"
#include "BLI_map.hh"
#include "BLI_math_vector.h"
#include "BLI_set.hh"
#include "BLI_sort.hh"
#include "BLI_string.h"
#include "BLI_string_ref.hh"
#include "BKE_context.hh"
#include "BKE_curve_legacy_convert.hh"
#include "BKE_geometry_set.hh"
#include "BKE_instances.hh"
#include "BKE_layer.hh"
#include "BKE_lib_id.hh"
#include "BKE_library.hh"
#include "BKE_object.hh"
#include "BKE_report.hh"
#include "DEG_depsgraph_build.hh"
#include "DNA_collection_types.h"
#include "obj_export_mtl.hh"
#include "obj_import_file_reader.hh"
#include "obj_import_mesh.hh"
#include "obj_import_nurbs.hh"
#include "obj_import_objects.hh"
#include "obj_importer.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.obj"};
namespace io::obj {
static Collection *find_or_create_collection(Main *bmain,
Collection *target,
const std::string &geom_name,
const OBJImportParams &import_params)
{
if (target == nullptr || import_params.collection_separator == 0) {
return target;
}
size_t subname_start = 0;
size_t sep_pos = geom_name.find(import_params.collection_separator);
if (sep_pos == std::string::npos) {
return target;
}
while (sep_pos != std::string::npos) {
/* Get current sub-name, find or create collection with that name. */
if (sep_pos > subname_start) {
std::string subname = geom_name.substr(subname_start, sep_pos - subname_start);
bool found = false;
for (CollectionChild &child : target->children) {
if (GS(child.collection->id.name) == ID_GR &&
STREQ(child.collection->id.name + 2, subname.c_str()))
{
target = child.collection;
found = true;
break;
}
}
if (!found) {
target = BKE_collection_add(bmain, target, subname.c_str());
}
}
/* Proceed to next sub-name component. */
subname_start = sep_pos + 1;
if (subname_start >= geom_name.size()) {
break;
}
sep_pos = geom_name.find(import_params.collection_separator, subname_start);
}
return target;
}
static void geometry_to_blender_geometry_set(const OBJImportParams &import_params,
const Span<std::unique_ptr<Geometry>> all_geometries,
const GlobalVertices &global_vertices,
Vector<bke::GeometrySet> &geometries)
{
for (const std::unique_ptr<Geometry> &geometry : all_geometries) {
bke::GeometrySet geometry_set;
if (geometry->geom_type_ == GEOM_MESH) {
MeshFromGeometry mesh_ob_from_geometry{*geometry, global_vertices};
Mesh *mesh = mesh_ob_from_geometry.create_mesh(import_params);
geometry_set = bke::GeometrySet::from_mesh(mesh);
}
else if (geometry->geom_type_ == GEOM_CURVE) {
CurveFromGeometry curve_ob_from_geometry(*geometry, global_vertices);
Curves *curves_id = curve_ob_from_geometry.create_curve(import_params);
geometry_set = bke::GeometrySet::from_curves(curves_id);
}
geometry_set.set_name(geometry->geometry_name_);
geometries.append(std::move(geometry_set));
}
}
/**
* Make Blender Mesh, Curve etc from Geometry and add them to the import collection.
*/
static void geometry_to_blender_objects(Main *bmain,
Scene *scene,
ViewLayer *view_layer,
const OBJImportParams &import_params,
MutableSpan<std::unique_ptr<Geometry>> all_geometries,
const GlobalVertices &global_vertices,
Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
Map<std::string, Material *> &created_materials)
{
LayerCollection *lc = BKE_layer_collection_get_active(view_layer);
/* Sort objects by name: creating many objects is much faster if the creation
* order is sorted by name. */
parallel_sort(all_geometries.begin(), all_geometries.end(), [](const auto &a, const auto &b) {
const char *na = a ? a->geometry_name_.c_str() : "";
const char *nb = b ? b->geometry_name_.c_str() : "";
return BLI_strcasecmp(na, nb) < 0;
});
/* Create all the objects. */
Vector<Object *> objects;
objects.reserve(all_geometries.size());
Set<Collection *> collections;
for (const std::unique_ptr<Geometry> &geometry : all_geometries) {
Object *obj = nullptr;
if (geometry->geom_type_ == GEOM_MESH) {
MeshFromGeometry mesh_ob_from_geometry{*geometry, global_vertices};
obj = mesh_ob_from_geometry.create_mesh_object(
bmain, materials, created_materials, import_params);
}
else if (geometry->geom_type_ == GEOM_CURVE) {
CurveFromGeometry curve_ob_from_geometry(*geometry, global_vertices);
obj = curve_ob_from_geometry.create_curve_object(bmain, import_params);
}
if (obj != nullptr) {
Collection *target_collection = find_or_create_collection(
bmain, lc->collection, geometry->geometry_name_, import_params);
collections.add(target_collection);
BKE_collection_object_add(bmain, target_collection, obj);
objects.append(obj);
}
}
/* Clamp object size if needed. */
if (import_params.clamp_size > 0.0f) {
std::optional<Bounds<float3>> bounds = std::nullopt;
for (Object *obj : objects) {
bounds = bounds::merge(bounds, BKE_object_boundbox_get(obj));
}
if (bounds.has_value()) {
const float max_diff = math::reduce_max(bounds->max - bounds->min);
if (import_params.clamp_size < max_diff * import_params.global_scale) {
const float scale = import_params.clamp_size / max_diff;
for (Object *obj : objects) {
copy_v3_fl(obj->scale, scale);
}
}
}
}
/* Do object selections in a separate loop (allows just one view layer sync). */
BKE_view_layer_synced_ensure(*bmain, scene, view_layer);
bool has_instantiated_object = false;
bool has_uninstantiated_object = false;
for (Object *obj : objects) {
Base *base = BKE_view_layer_base_find(view_layer, obj);
if (!base) {
/* Object not instantiated in current viewlayer. */
has_uninstantiated_object = true;
continue;
}
has_instantiated_object = true;
BKE_view_layer_base_select_and_set_active(view_layer, base);
int flags = ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY | ID_RECALC_ANIMATION |
ID_RECALC_BASE_FLAGS;
DEG_id_tag_update_ex(bmain, &obj->id, flags);
}
if (has_instantiated_object && has_uninstantiated_object) {
CLOG_ERROR(&LOG, "Some imported objects were not instantiated, while others were");
}
for (Collection *col : collections) {
DEG_id_tag_update(&col->id, ID_RECALC_SYNC_TO_EVAL);
}
DEG_id_tag_update(&scene->id, ID_RECALC_BASE_FLAGS);
DEG_relations_tag_update(bmain);
}
void importer_geometry(const OBJImportParams &import_params, Vector<bke::GeometrySet> &geometries)
{
/* List of geometries to be parsed from OBJ file. */
Vector<std::unique_ptr<Geometry>> all_geometries;
/* Container for vertex and UV vertex coordinates. */
GlobalVertices global_vertices;
OBJParser obj_parser{import_params};
obj_parser.parse(all_geometries, global_vertices);
geometry_to_blender_geometry_set(import_params, all_geometries, global_vertices, geometries);
}
void importer_main(bContext *C, const OBJImportParams &import_params)
{
Main *bmain = CTX_data_main(C);
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
/* List of geometries to be parsed from OBJ file. */
Vector<std::unique_ptr<Geometry>> all_geometries;
/* Container for vertex and UV vertex coordinates. */
GlobalVertices global_vertices;
/* List of MTLMaterial instances to be parsed from MTL file. */
Map<std::string, std::unique_ptr<MTLMaterial>> materials;
Map<std::string, Material *> created_materials;
OBJParser obj_parser{import_params};
obj_parser.parse(all_geometries, global_vertices);
/* Parse all referenced MTL files */
for (StringRefNull mtl_library : obj_parser.mtl_libraries()) {
MTLParser mtl_parser{mtl_library, import_params.filepath};
mtl_parser.parse_and_store(materials);
}
if (import_params.clear_selection) {
BKE_view_layer_base_deselect_all(*bmain, scene, view_layer);
}
LayerCollection *lc = BKE_layer_collection_get_active_editable(view_layer);
if (!ID_IS_EDITABLE(lc->collection)) {
BKE_report(import_params.reports,
RPT_WARNING,
"Could not find an editable collection in current scene, imported data will not be "
"instantiated");
}
/* Create Blender objects from the parsed geometries */
geometry_to_blender_objects(bmain,
scene,
view_layer,
import_params,
all_geometries,
global_vertices,
materials,
created_materials);
}
} // namespace io::obj
} // namespace blender

View File

@@ -0,0 +1,19 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup obj
*/
#pragma once
#include "IO_wavefront_obj.hh"
namespace blender::io::obj {
void importer_geometry(const OBJImportParams &import_params, Vector<bke::GeometrySet> &geometries);
void importer_main(bContext *C, const OBJImportParams &import_params);
} // namespace blender::io::obj

View File

@@ -0,0 +1,629 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include <gtest/gtest.h>
#include <memory>
#include <string>
#include <system_error>
#include "testing/testing.h"
#include "tests/blendfile_loading_base_test.h"
#include "BKE_appdir.hh"
#include "BKE_blender_version.h"
#include "BKE_gtest_base.hh"
#include "BKE_main.hh"
#include "BLI_fileops.h"
#include "BLI_string.h"
#include "BLO_readfile.hh"
#include "DEG_depsgraph.hh"
#include "obj_export_file_writer.hh"
#include "obj_export_nurbs.hh"
#include "obj_exporter.hh"
namespace blender::io::obj {
/* Set this true to keep comparison-failing test output in temp file directory. */
constexpr bool save_failing_test_output = false;
/* This is also the test name. */
class OBJExportTest : 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" SEP_STR "blend_scene" SEP_STR "all_objects.blend";
TEST_F(OBJExportTest, filter_objects_curves_as_mesh)
{
OBJExportParams params;
if (!load_file_and_depsgraph(all_objects_file)) {
ADD_FAILURE();
return;
}
auto [objmeshes, objcurves]{filter_supported_objects(depsgraph, params)};
EXPECT_EQ(objmeshes.size(), 21);
EXPECT_EQ(objcurves.size(), 0);
}
TEST_F(OBJExportTest, filter_objects_curves_as_nurbs)
{
OBJExportParams params;
if (!load_file_and_depsgraph(all_objects_file)) {
ADD_FAILURE();
return;
}
params.export_curves_as_nurbs = true;
auto [objmeshes, objcurves]{filter_supported_objects(depsgraph, params)};
EXPECT_EQ(objmeshes.size(), 18);
EXPECT_EQ(objcurves.size(), 3);
}
TEST_F(OBJExportTest, filter_objects_selected)
{
OBJExportParams params;
if (!load_file_and_depsgraph(all_objects_file)) {
ADD_FAILURE();
return;
}
params.export_selected_objects = true;
params.export_curves_as_nurbs = true;
auto [objmeshes, objcurves]{filter_supported_objects(depsgraph, 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] = SEP_STR "my_file.obj";
const char path_truth[FILE_MAX] = SEP_STR "my_file-0012.obj";
const int frame = -12;
char path_with_frame[FILE_MAX] = {0};
const bool ok = append_frame_to_filename(path_original, frame, path_with_frame);
EXPECT_TRUE(ok);
EXPECT_STREQ(path_with_frame, path_truth);
}
TEST(obj_exporter_utils, append_positive_frame_to_filename)
{
const char path_original[FILE_MAX] = SEP_STR "my_file.obj";
const char path_truth[FILE_MAX] = SEP_STR "my_file0012.obj";
const int frame = 12;
char path_with_frame[FILE_MAX] = {0};
const bool ok = append_frame_to_filename(path_original, frame, path_with_frame);
EXPECT_TRUE(ok);
EXPECT_STREQ(path_with_frame, path_truth);
}
TEST(obj_exporter_utils, append_large_positive_frame_to_filename)
{
const char path_original[FILE_MAX] = SEP_STR "my_file.obj";
const char path_truth[FILE_MAX] = SEP_STR "my_file1234567.obj";
const int frame = 1234567;
char path_with_frame[FILE_MAX] = {0};
const bool ok = append_frame_to_filename(path_original, frame, path_with_frame);
EXPECT_TRUE(ok);
EXPECT_STREQ(path_with_frame, path_truth);
}
static std::string read_temp_file_in_string(const std::string &file_path)
{
std::string res;
size_t buffer_len;
char *buffer = BLI_file_read_text_as_mem(file_path.c_str(), 0, &buffer_len);
if (buffer != nullptr) {
res.assign(buffer, buffer_len);
MEM_delete(buffer);
}
return res;
}
class ObjExporterWriterTest : public testing::Test {
protected:
void SetUp() override
{
BKE_tempdir_init(nullptr);
}
void TearDown() override
{
BKE_tempdir_session_purge();
}
std::string get_temp_obj_filename()
{
/* Use Latin Capital Letter A with Ogonek, Cyrillic Capital Letter Zhe
* at the end, to test I/O on non-English file names. */
const char *const temp_file_path = "output\xc4\x84\xd0\x96.OBJ";
return std::string(BKE_tempdir_session()) + SEP_STR + std::string(temp_file_path);
}
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) {
fprintf(stderr, "[%s] %s\n", ex.code().category().name(), ex.what());
return nullptr;
}
}
};
TEST_F(ObjExporterWriterTest, header)
{
/* Because testing doesn't fully initialize Blender, we need the following. */
BKE_tempdir_init(nullptr);
std::string out_file_path = get_temp_obj_filename();
{
OBJExportParams params;
std::unique_ptr<OBJWriter> writer = init_writer(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");
}
TEST_F(ObjExporterWriterTest, mtllib)
{
std::string out_file_path = get_temp_obj_filename();
{
OBJExportParams params;
std::unique_ptr<OBJWriter> writer = init_writer(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");
}
TEST(obj_exporter_writer, format_handler_buffer_chunking)
{
/* Use a tiny buffer chunk size, so that the test below ends up creating several blocks. */
FormatHandler h(16);
h.write_obj_object("abc");
h.write_obj_object("abcd");
h.write_obj_object("abcde");
h.write_obj_object("abcdef");
h.write_obj_object("012345678901234567890123456789abcd");
h.write_obj_object("123");
h.write_obj_curve_begin();
h.write_obj_newline();
h.write_obj_nurbs_parm_begin();
h.write_obj_nurbs_parm(0.0f);
h.write_obj_newline();
size_t got_blocks = h.get_block_count();
ASSERT_EQ(got_blocks, 6);
std::string got_string = h.get_as_string();
using namespace std::string_literals;
const char *expected = R"(o abc
o abcd
o abcde
o abcdef
o 012345678901234567890123456789abcd
o 123
curv
parm u 0.000000
)";
ASSERT_EQ(got_string, expected);
}
/* 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)
{
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) {
printf("No newline found in evaluated string\n");
return false;
}
if (b_next == std::string::npos) {
printf("No newline found in the golden string\n");
return false;
}
const size_t a_sublen = a_len - a_next;
const size_t b_sublen = b_len - b_next;
if (a_sublen != b_sublen) {
printf("Mismatching string length, evaluated contains %zu chars, while golden has %zu\n",
a_sublen,
b_sublen);
}
if (a.compare(a_next, a_sublen, b, b_next, b_sublen) != 0) {
for (int i = 0; i < std::min(a_sublen, b_sublen); ++i) {
if (a[a_next + i] != b[b_next + i]) {
printf("Difference found at pos %zu of a\n", a_next + i);
printf("a: %s ...\n", a.substr(a_next + i, 100).c_str());
printf("b: %s ...\n", b.substr(b_next + i, 100).c_str());
return false;
}
}
return false;
}
return true;
}
/* From here on, tests are whole file tests, testing for golden output. */
class OBJExportRegressionTest : public OBJExportTest {
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(nullptr);
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());
params.blen_filepath = bfile->main->filepath;
std::string golden_file_path = tests::flags_test_asset_dir() + SEP_STR + golden_obj;
BLI_path_split_dir_part(
golden_file_path.c_str(), params.file_base_for_tests, sizeof(params.file_base_for_tests));
export_frame(depsgraph, params, out_file_path.c_str());
std::string output_str = read_temp_file_in_string(out_file_path);
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 (!are_equal) {
printf("failed test for file: %s\n", golden_file_path.c_str());
if (save_failing_test_output) {
printf("failing test output in %s\n", out_file_path.c_str());
}
}
ASSERT_TRUE(are_equal);
if (!save_failing_test_output || are_equal) {
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 = tests::flags_test_asset_dir() + SEP_STR + golden_mtl;
std::string golden_mtl_str = read_temp_file_in_string(golden_mtl_file_path);
are_equal = strings_equal_after_first_lines(output_mtl_str, golden_mtl_str);
if (!are_equal) {
printf("failed test for mtl file: %s\n", golden_mtl_file_path.c_str());
if (save_failing_test_output) {
printf("failing test output in %s\n", out_mtl_file_path.c_str());
}
}
ASSERT_TRUE(are_equal);
if (!save_failing_test_output || are_equal) {
BLI_delete(out_mtl_file_path.c_str(), false, false);
}
}
}
};
TEST_F(OBJExportRegressionTest, all_tris)
{
OBJExportParams params;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "all_tris.blend",
"io_tests" SEP_STR "obj" SEP_STR "all_tris.obj",
"io_tests" SEP_STR "obj" SEP_STR "all_tris.mtl",
params);
}
TEST_F(OBJExportRegressionTest, all_quads)
{
OBJExportParams params;
params.global_scale = 2.0f;
params.export_materials = false;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "all_quads.blend",
"io_tests" SEP_STR "obj" SEP_STR "all_quads.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, fgons)
{
OBJExportParams params;
params.forward_axis = IO_AXIS_Y;
params.up_axis = IO_AXIS_Z;
params.export_materials = false;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "fgons.blend",
"io_tests" SEP_STR "obj" SEP_STR "fgons.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, edges)
{
OBJExportParams params;
params.forward_axis = IO_AXIS_Y;
params.up_axis = IO_AXIS_Z;
params.export_materials = false;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "edges.blend",
"io_tests" SEP_STR "obj" SEP_STR "edges.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, vertices)
{
OBJExportParams params;
params.forward_axis = IO_AXIS_Y;
params.up_axis = IO_AXIS_Z;
params.export_materials = false;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR
"cube_loose_edges_verts.blend",
"io_tests" SEP_STR "obj" SEP_STR "cube_loose_edges_verts.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, cube_loose_edges)
{
OBJExportParams params;
params.forward_axis = IO_AXIS_Y;
params.up_axis = IO_AXIS_Z;
params.export_materials = false;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "vertices.blend",
"io_tests" SEP_STR "obj" SEP_STR "vertices.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, non_uniform_scale)
{
OBJExportParams params;
params.export_materials = false;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR
"non_uniform_scale.blend",
"io_tests" SEP_STR "obj" SEP_STR "non_uniform_scale.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, nurbs_as_nurbs)
{
OBJExportParams params;
params.forward_axis = IO_AXIS_Y;
params.up_axis = IO_AXIS_Z;
params.export_materials = false;
params.export_curves_as_nurbs = true;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "nurbs.blend",
"io_tests" SEP_STR "obj" SEP_STR "nurbs.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, nurbs_curves_as_nurbs)
{
OBJExportParams params;
params.forward_axis = IO_AXIS_Y;
params.up_axis = IO_AXIS_Z;
params.export_materials = false;
params.export_curves_as_nurbs = true;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "nurbs_curves.blend",
"io_tests" SEP_STR "obj" SEP_STR "nurbs_curves.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, nurbs_as_mesh)
{
OBJExportParams params;
params.forward_axis = IO_AXIS_Y;
params.up_axis = IO_AXIS_Z;
params.export_materials = false;
params.export_curves_as_nurbs = false;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "nurbs.blend",
"io_tests" SEP_STR "obj" SEP_STR "nurbs_mesh.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, cube_all_data_triangulated)
{
OBJExportParams params;
params.forward_axis = IO_AXIS_Y;
params.up_axis = IO_AXIS_Z;
params.export_materials = false;
params.export_triangulated_mesh = true;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "cube_all_data.blend",
"io_tests" SEP_STR "obj" SEP_STR "cube_all_data_triangulated.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, cube_normal_edit)
{
OBJExportParams params;
params.forward_axis = IO_AXIS_Y;
params.up_axis = IO_AXIS_Z;
params.export_materials = false;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR
"cube_normal_edit.blend",
"io_tests" SEP_STR "obj" SEP_STR "cube_normal_edit.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, cube_vertex_groups)
{
OBJExportParams params;
params.export_materials = false;
params.export_normals = false;
params.export_uv = false;
params.export_vertex_groups = true;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR
"cube_vertex_groups.blend",
"io_tests" SEP_STR "obj" SEP_STR "cube_vertex_groups.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, cubes_positioned)
{
OBJExportParams params;
params.export_materials = false;
params.global_scale = 2.0f;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR
"cubes_positioned.blend",
"io_tests" SEP_STR "obj" SEP_STR "cubes_positioned.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, cubes_vertex_colors)
{
OBJExportParams params;
params.export_colors = true;
params.export_normals = false;
params.export_uv = false;
params.export_materials = false;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR
"cubes_vertex_colors.blend",
"io_tests" SEP_STR "obj" SEP_STR "cubes_vertex_colors.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, cubes_with_textures_strip)
{
OBJExportParams params;
params.path_mode = PATH_REFERENCE_STRIP;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR
"cubes_with_textures.blend",
"io_tests" SEP_STR "obj" SEP_STR "cubes_with_textures.obj",
"io_tests" SEP_STR "obj" SEP_STR "cubes_with_textures.mtl",
params);
}
TEST_F(OBJExportRegressionTest, cubes_with_textures_relative)
{
OBJExportParams params;
params.path_mode = PATH_REFERENCE_RELATIVE;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR
"cubes_with_textures.blend",
"io_tests" SEP_STR "obj" SEP_STR "cubes_with_textures_rel.obj",
"io_tests" SEP_STR "obj" SEP_STR "cubes_with_textures_rel.mtl",
params);
}
TEST_F(OBJExportRegressionTest, suzanne_all_data)
{
OBJExportParams params;
params.forward_axis = IO_AXIS_Y;
params.up_axis = IO_AXIS_Z;
params.export_materials = false;
params.export_smooth_groups = true;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR
"suzanne_all_data.blend",
"io_tests" SEP_STR "obj" SEP_STR "suzanne_all_data.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, all_curves)
{
OBJExportParams params;
params.export_materials = false;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_scene" SEP_STR "all_curves.blend",
"io_tests" SEP_STR "obj" SEP_STR "all_curves.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, all_curves_as_nurbs)
{
OBJExportParams params;
params.export_materials = false;
params.export_curves_as_nurbs = true;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_scene" SEP_STR "all_curves.blend",
"io_tests" SEP_STR "obj" SEP_STR "all_curves_as_nurbs.obj",
"",
params);
}
TEST_F(OBJExportRegressionTest, all_objects)
{
OBJExportParams params;
params.forward_axis = IO_AXIS_Y;
params.up_axis = IO_AXIS_Z;
params.export_smooth_groups = true;
params.export_colors = true;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_scene" SEP_STR "all_objects.blend",
"io_tests" SEP_STR "obj" SEP_STR "all_objects.obj",
"io_tests" SEP_STR "obj" SEP_STR "all_objects.mtl",
params);
}
TEST_F(OBJExportRegressionTest, all_objects_mat_groups)
{
OBJExportParams params;
params.forward_axis = IO_AXIS_Y;
params.up_axis = IO_AXIS_Z;
params.export_smooth_groups = true;
params.export_material_groups = true;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_scene" SEP_STR "all_objects.blend",
"io_tests" SEP_STR "obj" SEP_STR "all_objects_mat_groups.obj",
"io_tests" SEP_STR "obj" SEP_STR "all_objects_mat_groups.mtl",
params);
}
TEST_F(OBJExportRegressionTest, materials_without_pbr)
{
OBJExportParams params;
params.export_normals = false;
params.path_mode = PATH_REFERENCE_RELATIVE;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "materials_pbr.blend",
"io_tests" SEP_STR "obj" SEP_STR "materials_without_pbr.obj",
"io_tests" SEP_STR "obj" SEP_STR "materials_without_pbr.mtl",
params);
}
TEST_F(OBJExportRegressionTest, materials_pbr)
{
OBJExportParams params;
params.export_normals = false;
params.path_mode = PATH_REFERENCE_RELATIVE;
params.export_pbr_extensions = true;
compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "materials_pbr.blend",
"io_tests" SEP_STR "obj" SEP_STR "materials_pbr.obj",
"io_tests" SEP_STR "obj" SEP_STR "materials_pbr.mtl",
params);
}
} // namespace blender::io::obj

View File

@@ -0,0 +1,285 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include <gtest/gtest.h>
#include "BLI_fileops.h"
#include "BKE_appdir.hh"
#include "BKE_gtest_base.hh"
#include "CLG_log.h"
#include "testing/testing.h"
#include "obj_export_mtl.hh"
#include "obj_import_file_reader.hh"
namespace blender::io::obj {
class OBJMTLParserTest : public bke::BlenderGTestBase {
public:
void check_string(const char *text, const MTLMaterial *expect, size_t expect_count)
{
BKE_tempdir_init(nullptr);
std::string tmp_dir = BKE_tempdir_base();
std::string tmp_file_name = "mtl_test.mtl";
std::string tmp_file_path = tmp_dir + SEP_STR + tmp_file_name;
FILE *tmp_file = BLI_fopen(tmp_file_path.c_str(), "wb");
fputs(text, tmp_file);
fclose(tmp_file);
check_impl(tmp_file_name, tmp_dir, expect, expect_count);
BLI_delete(tmp_file_path.c_str(), false, false);
}
void check(const char *file, const MTLMaterial *expect, size_t expect_count)
{
std::string obj_dir = tests::flags_test_asset_dir() +
(SEP_STR "io_tests" SEP_STR "obj" SEP_STR);
check_impl(file, obj_dir, expect, expect_count);
}
void check_impl(StringRefNull mtl_file_path,
StringRefNull file_dir,
const MTLMaterial *expect,
size_t expect_count)
{
MTLParser parser(mtl_file_path, file_dir + "dummy.obj");
Map<std::string, std::unique_ptr<MTLMaterial>> materials;
parser.parse_and_store(materials);
for (int i = 0; i < expect_count; ++i) {
const MTLMaterial &exp = expect[i];
if (!materials.contains(exp.name)) {
fprintf(stderr, "Material '%s' was expected in parsed result\n", exp.name.c_str());
ADD_FAILURE();
continue;
}
const MTLMaterial &got = *materials.lookup(exp.name);
const float tol = 0.0001f;
EXPECT_V3_NEAR(exp.ambient_color, got.ambient_color, tol);
EXPECT_V3_NEAR(exp.color, got.color, tol);
EXPECT_V3_NEAR(exp.spec_color, got.spec_color, tol);
EXPECT_V3_NEAR(exp.emission_color, got.emission_color, tol);
EXPECT_V3_NEAR(exp.transmit_color, got.transmit_color, tol);
EXPECT_NEAR(exp.spec_exponent, got.spec_exponent, tol);
EXPECT_NEAR(exp.ior, got.ior, tol);
EXPECT_NEAR(exp.alpha, got.alpha, tol);
EXPECT_NEAR(exp.normal_strength, got.normal_strength, tol);
EXPECT_EQ(exp.illum_mode, got.illum_mode);
EXPECT_NEAR(exp.roughness, got.roughness, tol);
EXPECT_NEAR(exp.metallic, got.metallic, tol);
EXPECT_NEAR(exp.sheen, got.sheen, tol);
EXPECT_NEAR(exp.cc_thickness, got.cc_thickness, tol);
EXPECT_NEAR(exp.cc_roughness, got.cc_roughness, tol);
EXPECT_NEAR(exp.aniso, got.aniso, tol);
EXPECT_NEAR(exp.aniso_rot, got.aniso_rot, tol);
for (int key = 0; key < int(MTLTexMapType::Count); key++) {
const MTLTexMap &exp_tex = exp.texture_maps[key];
const MTLTexMap &got_tex = got.texture_maps[key];
EXPECT_STREQ(exp_tex.image_path.c_str(), got_tex.image_path.c_str());
EXPECT_V3_NEAR(exp_tex.translation, got_tex.translation, tol);
EXPECT_V3_NEAR(exp_tex.scale, got_tex.scale, tol);
EXPECT_EQ(exp_tex.projection_type, got_tex.projection_type);
}
}
EXPECT_EQ(materials.size(), expect_count);
}
};
TEST_F(OBJMTLParserTest, string_newlines_whitespace)
{
const char *text =
"# a comment\n"
" # indented comment\n"
"# comment with CRLF line ending\r\n"
"\r\n"
"newmtl simple\n"
"Ka 0.1 0.2 0.3\n"
"illum 4\n"
"newmtl\ttab_indentation\n"
"Kd\t \t0.2 0.3\t0.4 \t \n"
"newmtl space_after_name \t \n"
"Ks 0.4 0.5 0.6\n"
"newmtl space_before_name\n"
"newmtl indented_values\n"
" Ka 0.5 0.6 0.7\n"
"\t\t\tKd 0.6 0.7 0.8\n"
"newmtl crlf_ending\r\n"
"Ns 5.0\r\n"
"map_Kd sometex_d.png\r\n"
"map_Ks sometex_s_spaces_after_name.png \t \r\n";
MTLMaterial mat[6];
mat[0].name = "simple";
mat[0].ambient_color = {0.1f, 0.2f, 0.3f};
mat[0].illum_mode = 4;
mat[1].name = "tab_indentation";
mat[1].color = {0.2f, 0.3f, 0.4f};
mat[2].name = "space_after_name";
mat[2].spec_color = {0.4f, 0.5f, 0.6f};
mat[3].name = "space_before_name";
mat[4].name = "indented_values";
mat[4].ambient_color = {0.5f, 0.6f, 0.7f};
mat[4].color = {0.6f, 0.7f, 0.8f};
mat[5].name = "crlf_ending";
mat[5].spec_exponent = 5.0f;
mat[5].tex_map_of_type(MTLTexMapType::Color).image_path = "sometex_d.png";
mat[5].tex_map_of_type(MTLTexMapType::Specular).image_path = "sometex_s_spaces_after_name.png";
check_string(text, mat, ARRAY_SIZE(mat));
}
TEST_F(OBJMTLParserTest, materials)
{
MTLMaterial mat[6];
mat[0].name = "no_textures_red";
mat[0].ambient_color = {0.3f, 0.3f, 0.3f};
mat[0].color = {0.8f, 0.3f, 0.1f};
mat[0].spec_exponent = 5.624998f;
mat[1].name = "four_maps";
mat[1].ambient_color = {1, 1, 1};
mat[1].color = {0.8f, 0.8f, 0.8f};
mat[1].spec_color = {0.5f, 0.5f, 0.5f};
mat[1].emission_color = {0, 0, 0};
mat[1].spec_exponent = 1000;
mat[1].ior = 1.45f;
mat[1].alpha = 1;
mat[1].illum_mode = 2;
mat[1].normal_strength = 1;
{
MTLTexMap &kd = mat[1].tex_map_of_type(MTLTexMapType::Color);
kd.image_path = "texture.png";
MTLTexMap &ns = mat[1].tex_map_of_type(MTLTexMapType::SpecularExponent);
ns.image_path = "sometexture_Roughness.png";
MTLTexMap &refl = mat[1].tex_map_of_type(MTLTexMapType::Reflection);
refl.image_path = "sometexture_Metallic.png";
MTLTexMap &bump = mat[1].tex_map_of_type(MTLTexMapType::Normal);
bump.image_path = "sometexture_Normal.png";
}
mat[2].name = "Clay";
mat[2].ambient_color = {1, 1, 1};
mat[2].color = {0.8f, 0.682657f, 0.536371f};
mat[2].spec_color = {0.5f, 0.5f, 0.5f};
mat[2].emission_color = {0, 0, 0};
mat[2].spec_exponent = 440.924042f;
mat[2].ior = 1.45f;
mat[2].alpha = 1;
mat[2].illum_mode = 2;
mat[3].name = "Hat";
mat[3].ambient_color = {1, 1, 1};
mat[3].color = {0.8f, 0.8f, 0.8f};
mat[3].spec_color = {0.5f, 0.5f, 0.5f};
mat[3].spec_exponent = 800;
mat[3].normal_strength = 0.5f;
{
MTLTexMap &kd = mat[3].tex_map_of_type(MTLTexMapType::Color);
kd.image_path = "someHatTexture_BaseColor.jpg";
MTLTexMap &ns = mat[3].tex_map_of_type(MTLTexMapType::SpecularExponent);
ns.image_path = "someHatTexture_Roughness.jpg";
MTLTexMap &refl = mat[3].tex_map_of_type(MTLTexMapType::Reflection);
refl.image_path = "someHatTexture_Metalness.jpg";
MTLTexMap &bump = mat[3].tex_map_of_type(MTLTexMapType::Normal);
bump.image_path = "someHatTexture_Normal.jpg";
}
mat[4].name = "Parser_Test";
mat[4].ambient_color = {0.1f, 0.2f, 0.3f};
mat[4].color = {0.4f, 0.5f, 0.6f};
mat[4].spec_color = {0.7f, 0.8f, 0.9f};
mat[4].illum_mode = 6;
mat[4].spec_exponent = 15.5;
mat[4].ior = 1.5;
mat[4].alpha = 0.5;
mat[4].normal_strength = 0.1f;
mat[4].transmit_color = {0.1f, 0.3f, 0.5f};
mat[4].normal_strength = 0.1f;
mat[4].roughness = 0.2f;
mat[4].metallic = 0.3f;
mat[4].sheen = 0.4f;
mat[4].cc_thickness = 0.5f;
mat[4].cc_roughness = 0.6f;
mat[4].aniso = 0.7f;
mat[4].aniso_rot = 0.8f;
{
MTLTexMap &kd = mat[4].tex_map_of_type(MTLTexMapType::Color);
kd.image_path = "sometex_d.png";
MTLTexMap &ns = mat[4].tex_map_of_type(MTLTexMapType::SpecularExponent);
ns.image_path = "sometex_ns.psd";
MTLTexMap &refl = mat[4].tex_map_of_type(MTLTexMapType::Reflection);
refl.image_path = "clouds.tiff";
refl.scale = {1.5f, 2.5f, 3.5f};
refl.translation = {4.5f, 5.5f, 6.5f};
refl.projection_type = SHD_PROJ_SPHERE;
MTLTexMap &bump = mat[4].tex_map_of_type(MTLTexMapType::Normal);
bump.image_path = "somebump.tga";
bump.scale = {3, 4, 5};
}
mat[5].name = "Parser_ScaleOffset_Test";
{
MTLTexMap &kd = mat[5].tex_map_of_type(MTLTexMapType::Color);
kd.translation = {2.5f, 0.0f, 0.0f};
kd.image_path = "OffsetOneValue.png";
MTLTexMap &ks = mat[5].tex_map_of_type(MTLTexMapType::Specular);
ks.scale = {1.5f, 2.5f, 1.0f};
ks.translation = {3.5f, 4.5f, 0.0f};
ks.image_path = "ScaleOffsetBothTwovalues.png";
MTLTexMap &ns = mat[5].tex_map_of_type(MTLTexMapType::SpecularExponent);
ns.scale = {0.5f, 1.0f, 1.0f};
ns.image_path = "1.Value.png";
}
check("materials.mtl", mat, ARRAY_SIZE(mat));
}
TEST_F(OBJMTLParserTest, materials_pbr)
{
MTLMaterial mat[2];
mat[0].name = "Mat1";
mat[0].color = {0.8f, 0.276449f, 0.101911f};
mat[0].spec_color = {0.25f, 0.25f, 0.25f};
mat[0].emission_color = {0, 0, 0};
mat[0].ior = 1.45f;
mat[0].alpha = 1;
mat[0].illum_mode = 3;
mat[0].roughness = 0.4f;
mat[0].metallic = 0.9f;
mat[0].sheen = 0.06f;
mat[0].cc_thickness = 0.393182f;
mat[0].cc_roughness = 0.05f;
mat[0].aniso = 0.2f;
mat[0].aniso_rot = 0.0f;
mat[1].name = "Mat2";
mat[1].color = {0.8f, 0.8f, 0.8f};
mat[1].spec_color = {0.5f, 0.5f, 0.5f};
mat[1].ior = 1.45f;
mat[1].alpha = 1;
mat[1].illum_mode = 2;
mat[1].metallic = 0.0f;
mat[1].cc_thickness = 0.3f;
mat[1].cc_roughness = 0.4f;
mat[1].aniso = 0.8f;
mat[1].aniso_rot = 0.7f;
{
MTLTexMap &pr = mat[1].tex_map_of_type(MTLTexMapType::Roughness);
pr.image_path = "../blend_geometry/texture_roughness.png";
MTLTexMap &ps = mat[1].tex_map_of_type(MTLTexMapType::Sheen);
ps.image_path = "../blend_geometry/texture_checker.png";
MTLTexMap &ke = mat[1].tex_map_of_type(MTLTexMapType::Emission);
ke.image_path = "../blend_geometry/texture_illum.png";
}
check("materials_pbr.mtl", mat, ARRAY_SIZE(mat));
}
} // namespace blender::io::obj

View File

@@ -0,0 +1,570 @@
/* SPDX-FileCopyrightText: 2023-2025 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include "BLI_array_utils.hh"
#include "BLI_string.h"
#include "BKE_appdir.hh"
#include "BKE_curves.hh"
#include "BKE_gtest_base.hh"
#include "obj_export_file_writer.hh"
#include "obj_export_nurbs.hh"
#include "obj_exporter.hh"
#include "obj_importer.hh"
#include "testing/testing.h"
namespace blender::io::obj {
static OBJExportParams default_export_params(const std::string &filepath)
{
OBJExportParams params;
params.forward_axis = eIOAxis::IO_AXIS_Y;
params.up_axis = eIOAxis::IO_AXIS_Z;
STRNCPY(params.filepath, filepath.c_str());
return params;
}
static OBJImportParams default_import_params(const std::string &filepath)
{
OBJImportParams params;
params.forward_axis = eIOAxis::IO_AXIS_Y;
params.up_axis = eIOAxis::IO_AXIS_Z;
STRNCPY(params.filepath, filepath.c_str());
return params;
}
class OBJCurvesTest : public bke::BlenderGTestBase {
public:
void write_curves(const Span<std::unique_ptr<IOBJCurve>> curves, OBJExportParams params)
{
export_objects(params, Span<std::unique_ptr<OBJMesh>>(nullptr, 0), curves, params.filepath);
}
void write_curves(const std::unique_ptr<IOBJCurve> &curve, OBJExportParams params)
{
Span<std::unique_ptr<IOBJCurve>> span(&curve, 1);
write_curves(span, params);
}
void write_curves(const bke::CurvesGeometry &curve, OBJExportParams params)
{
float4x4 identity = float4x4::identity();
std::unique_ptr<IOBJCurve> curve_wrapper(new OBJCurves(curve, identity, "test"));
write_curves(curve_wrapper, params);
}
Vector<bke::GeometrySet> read_curves(OBJImportParams params)
{
Vector<bke::GeometrySet> geoms;
importer_geometry(params, geoms);
return geoms;
}
static bke::CurvesGeometry create_curves(Span<float3> points, bool cyclic)
{
bke::CurvesGeometry curves(points.size(), 1);
curves.offsets_for_write()[0] = 0;
curves.offsets_for_write()[1] = points.size();
curves.cyclic_for_write()[0] = cyclic;
curves.positions_for_write().copy_from(points);
return curves;
}
static bke::CurvesGeometry create_rational_nurbs(
Span<float3> points, Span<float> weights, bool cyclic, int8_t order, KnotsMode mode)
{
bke::CurvesGeometry curves = create_curves(points, cyclic);
curves.nurbs_orders_for_write()[0] = order;
curves.nurbs_knots_modes_for_write()[0] = int8_t(mode);
curves.nurbs_weights_for_write().copy_from(weights);
return curves;
}
static bke::CurvesGeometry create_nurbs(Span<float3> points,
bool cyclic,
int8_t order,
KnotsMode mode)
{
bke::CurvesGeometry curves = create_curves(points, cyclic);
curves.nurbs_orders_for_write()[0] = order;
curves.nurbs_knots_modes_for_write()[0] = int8_t(mode);
curves.nurbs_weights_for_write().fill(1.0f);
return curves;
}
void run_nurbs_test(const Span<float3> points,
const int8_t order,
const KnotsMode mode,
const bool cyclic,
bke::CurvesGeometry &src_curve,
const bke::CurvesGeometry *&result_curve,
Span<float3> expected_points = Span<float3>(),
const KnotsMode *expected_mode = nullptr,
const bool *expected_cyclic = nullptr)
{
BKE_tempdir_init(nullptr);
std::string tempdir = std::string(BKE_tempdir_base());
std::string out_file_path = tempdir + BLI_path_basename("io_obj/tmp_6f5273f4.obj");
/* Write/Read */
src_curve = OBJCurvesTest::create_nurbs(points, cyclic, order, mode);
ASSERT_TRUE(src_curve.cyclic()[0] == cyclic); /* Validate test function */
write_curves(src_curve, default_export_params(out_file_path));
Vector<bke::GeometrySet> result = read_curves(default_import_params(out_file_path));
ASSERT_TRUE(result.size() == 1);
result_curve = &result[0].get_curves()->geometry.wrap();
/* Validate properties */
EXPECT_EQ(result_curve->nurbs_orders()[0], order);
EXPECT_EQ(result_curve->cyclic()[0], expected_cyclic ? *expected_cyclic : cyclic);
EXPECT_EQ(result_curve->nurbs_knots_modes()[0], int8_t(expected_mode ? *expected_mode : mode));
const Span<float3> result_points = result_curve->positions();
expected_points = expected_points.size() ? expected_points : points;
ASSERT_EQ(expected_points.size(), result_points.size());
EXPECT_NEAR_ARRAY_ND(
expected_points.data(), result_points.data(), expected_points.size(), 3, 1e-4);
if (result_curve->nurbs_knots_modes()[0] != KnotsMode::NURBS_KNOT_MODE_CUSTOM) {
ASSERT_TRUE(result_curve->custom_knots == nullptr);
}
}
};
const std::array<float3, 13> position_array{float3{1.0f, -1.0f, 2.0f},
float3{2.0f, -2.0f, 4.0f},
float3{3.0f, -3.0f, 6.0f},
float3{4.0f, -4.0f, 8.0f},
float3{5.0f, -5.0f, 10.0f},
float3{6.0f, -6.0f, 12.0f},
float3{7.0f, -7.0f, 14.0f},
float3{1.0f / 4.0f, -2.0f, 3.0f / 6.0f},
float3{1.0f / 6.0f, -3.0f, 3.0f / 9.0f},
float3{1.0f / 8.0f, -4.0f, 3.0f / 12.0f},
float3{1.0f / 5.0f, -5.0f, 3.0f / 11.0f},
float3{1.0f / 3.0f, -6.0f, 3.0f / 10.0f},
float3{1.0f / 2.0f, -7.0f, 3.0f / 9.0f}};
const Span<float3> position_data = Span<float3>(position_array);
/* -------------------------------------------------------------------- */
/** \name Knot vector: KnotMode::NURBS_KNOT_MODE_NORMAL
* \{ */
TEST_F(OBJCurvesTest, nurbs_io_uniform_polyline)
{
const int8_t order = 2;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_NORMAL;
const bool cyclic = false;
const Span<float3> positions = position_data.slice(0, 5);
const KnotsMode expected_mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT;
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions, order, mode, cyclic, src, result, positions, &expected_mode);
/* Validate uniform knots, don't do this in general as it only verifies the knot generator
* `bke::curves::nurbs::calculate_knots`. */
Vector<float> knot_buffer(bke::curves::nurbs::knots_num(positions.size(), order, cyclic));
bke::curves::nurbs::calculate_knots(positions.size(), mode, order, cyclic, knot_buffer);
const Vector<int> multiplicity = bke::curves::nurbs::calculate_multiplicity_sequence(
knot_buffer);
std::array<int, 7> expected_mult;
std::fill(expected_mult.begin(), expected_mult.end(), 1);
EXPECT_EQ_SPAN<int>(multiplicity, expected_mult);
}
TEST_F(OBJCurvesTest, nurbs_io_uniform_deg5)
{
const int8_t order = 6;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_NORMAL;
const Span<float3> positions = position_data.slice(0, 8);
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions, order, mode, false, src, result);
}
TEST_F(OBJCurvesTest, nurbs_io_uniform_clamped_polyline)
{
const int8_t order = 2;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT;
const Span<float3> positions = position_data.slice(0, 5);
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions, order, mode, false, src, result);
}
TEST_F(OBJCurvesTest, nurbs_io_uniform_endpoint_clamped_deg3)
{
const int8_t order = 3;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT;
const Span<float3> positions = position_data.slice(0, 5);
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions, order, mode, false, src, result);
}
TEST_F(OBJCurvesTest, nurbs_io_uniform_endpoint_clamped_deg5)
{
const int8_t order = 6;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_NORMAL;
const Span<float3> positions = position_data.slice(0, 8);
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions, order, mode, false, src, result);
}
TEST_F(OBJCurvesTest, nurbs_io_uniform_cyclic_polyline)
{
const int8_t order = 2;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_NORMAL;
const Span<float3> positions = position_data.slice(0, 5);
const KnotsMode expected_mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT;
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions, order, mode, true, src, result, positions, &expected_mode);
}
TEST_F(OBJCurvesTest, nurbs_io_uniform_cyclic_deg4)
{
const int8_t order = 5;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_NORMAL;
const Span<float3> positions = position_data.slice(0, 8);
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions, order, mode, true, src, result);
}
TEST_F(OBJCurvesTest, nurbs_io_uniform_cyclic_clamped_deg4)
{
const int8_t order = 5;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT;
const Span<float3> positions = position_data.slice(0, 12);
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions, order, mode, true, src, result);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Knot vector: KnotMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER
* \{ */
TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_single_segment_deg2)
{
const int8_t order = 3;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER;
const Span<float3> positions = position_data.slice(0, 3);
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions, order, mode, false, src, result);
}
TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_single_segment_deg4)
{
const int8_t order = 5;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER;
const Span<float3> positions = position_data.slice(0, 5);
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions, order, mode, false, src, result);
}
TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_deg2)
{
const int8_t order = 3;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER;
const Span<float3> positions = position_data.slice(0, 7);
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions, order, mode, false, src, result);
}
TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_uneven_deg2)
{
const int8_t order = 3;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER;
const Span<float3> positions = position_data.slice(0, 8);
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions, order, mode, false, src, result, positions.slice(0, 7));
}
TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_deg4)
{
const int8_t order = 5;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER;
const Span<float3> positions = position_data.slice(0, 13);
/* Even (whole Bezier segments). */
{
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions, order, mode, false, src, result);
}
{
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions.slice(0, 9), order, mode, false, src, result);
}
/* Uneven (incomplete segment). */
{
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions.slice(0, 12), order, mode, false, src, result, positions.slice(0, 9));
}
{
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions.slice(0, 11), order, mode, false, src, result, positions.slice(0, 9));
}
{
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions.slice(0, 10), order, mode, false, src, result, positions.slice(0, 9));
}
}
TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_cyclic_deg4_looped_12)
{
const int8_t order = 5;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER;
const Span<float3> positions = position_data.slice(0, 12);
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions.slice(0, 12), order, mode, true, src, result);
}
TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_cyclic_deg4_looped_8)
{
const int8_t order = 5;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER;
const Span<float3> positions = position_data.slice(0, 8);
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions, order, mode, true, src, result);
}
TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_cyclic_deg4_discontinous_13)
{
const int8_t order = 5;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER;
const Span<float3> positions = position_data;
Vector<float3> expected(positions);
expected.append(positions[0]);
const bool expect_cyclic = false;
const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_CUSTOM;
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(
positions, order, mode, true, src, result, expected, &expect_mode, &expect_cyclic);
}
TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_cyclic_deg4_discontinous_11)
{
const int8_t order = 5;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER;
const Span<float3> positions = position_data.slice(0, 11);
Vector<float3> expected(positions);
expected.append(positions[0]);
const bool expect_cyclic = false;
const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_CUSTOM;
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(
positions, order, mode, true, src, result, expected, &expect_mode, &expect_cyclic);
}
TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_cyclic_deg4_discontinous_10)
{
const int8_t order = 5;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER;
const Span<float3> positions = position_data.slice(0, 10);
Vector<float3> expected(positions);
expected.append(positions[0]);
const bool expect_cyclic = false;
const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_CUSTOM;
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(
positions, order, mode, true, src, result, expected, &expect_mode, &expect_cyclic);
}
TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_cyclic_deg4_discontinous_9)
{
const int8_t order = 5;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER;
const Span<float3> positions = position_data.slice(0, 9);
Vector<float3> expected(positions);
expected.append(positions[0]);
const bool expect_cyclic = false;
const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_CUSTOM;
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(
positions, order, mode, true, src, result, expected, &expect_mode, &expect_cyclic);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Knot vector: KnotMode::NURBS_KNOT_MODE_BEZIER
* \{ */
TEST_F(OBJCurvesTest, nurbs_io_bezier_cyclic_deg4_looped_12)
{
const int8_t order = 5;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_BEZIER;
const Span<float3> positions = position_data.slice(0, 12);
Vector<float3> expected(positions.size());
array_utils::copy(positions.slice(1, 11), expected.as_mutable_span().slice(0, 11));
expected.last() = positions.first();
const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER;
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions, order, mode, true, src, result, expected, &expect_mode);
}
TEST_F(OBJCurvesTest, nurbs_io_bezier_cyclic_deg4_looped_discontinous_13)
{
const int8_t order = 5;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_BEZIER;
const Span<float3> positions = position_data.slice(0, 13);
Vector<float3> expected(positions.size() + 1);
array_utils::copy(positions.slice(1, 12), expected.as_mutable_span().slice(0, 12));
expected.last(1) = positions.first();
expected.last() = positions[1];
const bool expect_cyclic = false;
const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_CUSTOM;
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(
positions, order, mode, true, src, result, expected, &expect_mode, &expect_cyclic);
}
TEST_F(OBJCurvesTest, nurbs_io_bezier_cyclic_deg4_looped_discontinous_11)
{
const int8_t order = 5;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_BEZIER;
const Span<float3> positions = position_data.slice(0, 11);
Vector<float3> expected(positions.size() + 1);
array_utils::copy(positions.slice(1, 10), expected.as_mutable_span().slice(0, 10));
expected.last(1) = positions.first();
expected.last() = positions[1];
const bool expect_cyclic = false;
const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_CUSTOM;
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(
positions, order, mode, true, src, result, expected, &expect_mode, &expect_cyclic);
}
TEST_F(OBJCurvesTest, nurbs_io_bezier_cyclic_deg4_looped_discontinous_10)
{
const int8_t order = 5;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_BEZIER;
const Span<float3> positions = position_data.slice(0, 10);
Vector<float3> expected(positions.size() + 1);
array_utils::copy(positions.slice(1, 9), expected.as_mutable_span().slice(0, 9));
expected.last(1) = positions.first();
expected.last() = positions[1];
const bool expect_cyclic = false;
const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_CUSTOM;
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(
positions, order, mode, true, src, result, expected, &expect_mode, &expect_cyclic);
}
TEST_F(OBJCurvesTest, nurbs_io_bezier_cyclic_deg4_looped_discontinous_9)
{
const int8_t order = 5;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_BEZIER;
const Span<float3> positions = position_data.slice(0, 9);
Vector<float3> expected(positions.size() + 1);
array_utils::copy(positions.slice(1, 8), expected.as_mutable_span().slice(0, 8));
expected.last(1) = positions.first();
expected.last() = positions[1];
const bool expect_cyclic = false;
const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_CUSTOM;
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(
positions, order, mode, true, src, result, expected, &expect_mode, &expect_cyclic);
}
TEST_F(OBJCurvesTest, nurbs_io_bezier_cyclic_deg4_looped_8)
{
const int8_t order = 5;
const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_BEZIER;
const Span<float3> positions = position_data.slice(0, 8);
Vector<float3> expected(positions.size());
array_utils::copy(positions.slice(1, 7), expected.as_mutable_span().slice(0, 7));
expected.last() = positions.first();
const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER;
bke::CurvesGeometry src;
const bke::CurvesGeometry *result;
run_nurbs_test(positions, order, mode, true, src, result, expected, &expect_mode);
}
/** \} */
} // namespace blender::io::obj