Add Chromium-only Blender WebEngine parity work
This commit is contained in:
87
blender-5.2.0/source/blender/io/wavefront_obj/CMakeLists.txt
Normal file
87
blender-5.2.0/source/blender/io/wavefront_obj/CMakeLists.txt
Normal 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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ¶ms, 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>> ©_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, ©_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
|
||||
@@ -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>> ©_set);
|
||||
};
|
||||
} // namespace blender::io::obj
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 (°_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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ¶ms,
|
||||
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 ¶ms)
|
||||
{
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user