STL: Add new C++ based STL importer

A new experimentatl STL importer, written in C++. Roughly 7-9x faster than the
Python based one.

Reviewed By: Aras Pranckevicius, Hans Goudey.
Differential Revision: https://developer.blender.org/D14941
This commit is contained in:
Eyad Ahmed 2022-06-06 20:57:38 +03:00 committed by Aras Pranckevicius
parent 14fc89f38f
commit 7c511f1b47
31 changed files with 926 additions and 94 deletions

View File

@ -455,6 +455,7 @@ class TOPBAR_MT_file_import(Menu):
self.layout.operator("wm.gpencil_import_svg", text="SVG as Grease Pencil")
self.layout.operator("wm.obj_import", text="Wavefront (.obj) (experimental)")
self.layout.operator("wm.stl_import", text="STL (.stl) (experimental)")
class TOPBAR_MT_file_export(Menu):

View File

@ -13,6 +13,7 @@ set(INC
../../io/gpencil
../../io/usd
../../io/wavefront_obj
../../io/stl
../../makesdna
../../makesrna
../../windowmanager
@ -33,6 +34,7 @@ set(SRC
io_obj.c
io_ops.c
io_usd.c
io_stl_ops.c
io_alembic.h
io_cache.h
@ -41,12 +43,14 @@ set(SRC
io_obj.h
io_ops.h
io_usd.h
io_stl_ops.h
)
set(LIB
bf_blenkernel
bf_blenlib
bf_wavefront_obj
bf_stl
)
if(WITH_OPENCOLLADA)

View File

@ -31,28 +31,11 @@
#include "DEG_depsgraph.h"
#include "IO_orientation.h"
#include "IO_path_util_types.h"
#include "IO_wavefront_obj.h"
#include "io_obj.h"
static const EnumPropertyItem io_obj_transform_axis_forward[] = {
{OBJ_AXIS_X_FORWARD, "X_FORWARD", 0, "X", "Positive X axis"},
{OBJ_AXIS_Y_FORWARD, "Y_FORWARD", 0, "Y", "Positive Y axis"},
{OBJ_AXIS_Z_FORWARD, "Z_FORWARD", 0, "Z", "Positive Z axis"},
{OBJ_AXIS_NEGATIVE_X_FORWARD, "NEGATIVE_X_FORWARD", 0, "-X", "Negative X axis"},
{OBJ_AXIS_NEGATIVE_Y_FORWARD, "NEGATIVE_Y_FORWARD", 0, "-Y", "Negative Y axis"},
{OBJ_AXIS_NEGATIVE_Z_FORWARD, "NEGATIVE_Z_FORWARD", 0, "-Z", "Negative Z axis"},
{0, NULL, 0, NULL, NULL}};
static const EnumPropertyItem io_obj_transform_axis_up[] = {
{OBJ_AXIS_X_UP, "X_UP", 0, "X", "Positive X axis"},
{OBJ_AXIS_Y_UP, "Y_UP", 0, "Y", "Positive Y axis"},
{OBJ_AXIS_Z_UP, "Z_UP", 0, "Z", "Positive Z axis"},
{OBJ_AXIS_NEGATIVE_X_UP, "NEGATIVE_X_UP", 0, "-X", "Negative X axis"},
{OBJ_AXIS_NEGATIVE_Y_UP, "NEGATIVE_Y_UP", 0, "-Y", "Negative Y axis"},
{OBJ_AXIS_NEGATIVE_Z_UP, "NEGATIVE_Z_UP", 0, "-Z", "Negative Z axis"},
{0, NULL, 0, NULL, NULL}};
static const EnumPropertyItem io_obj_export_evaluation_mode[] = {
{DAG_EVAL_RENDER, "DAG_EVAL_RENDER", 0, "Render", "Export objects as they appear in render"},
{DAG_EVAL_VIEWPORT,
@ -296,13 +279,9 @@ void WM_OT_obj_export(struct wmOperatorType *ot)
INT_MIN,
INT_MAX);
/* Object transform options. */
RNA_def_enum(ot->srna,
"forward_axis",
io_obj_transform_axis_forward,
OBJ_AXIS_NEGATIVE_Z_FORWARD,
"Forward Axis",
"");
RNA_def_enum(ot->srna, "up_axis", io_obj_transform_axis_up, OBJ_AXIS_Y_UP, "Up Axis", "");
RNA_def_enum(
ot->srna, "forward_axis", io_transform_axis, IO_AXIS_NEGATIVE_Z, "Forward Axis", "");
RNA_def_enum(ot->srna, "up_axis", io_transform_axis, IO_AXIS_Y, "Up Axis", "");
RNA_def_float(ot->srna,
"scaling_factor",
1.0f,
@ -480,13 +459,9 @@ void WM_OT_obj_import(struct wmOperatorType *ot)
"Resize the objects to keep bounding box under this value. Value 0 disables clamping",
0.0f,
1000.0f);
RNA_def_enum(ot->srna,
"forward_axis",
io_obj_transform_axis_forward,
OBJ_AXIS_NEGATIVE_Z_FORWARD,
"Forward Axis",
"");
RNA_def_enum(ot->srna, "up_axis", io_obj_transform_axis_up, OBJ_AXIS_Y_UP, "Up Axis", "");
RNA_def_enum(
ot->srna, "forward_axis", io_transform_axis, IO_AXIS_NEGATIVE_Z, "Forward Axis", "");
RNA_def_enum(ot->srna, "up_axis", io_transform_axis, IO_AXIS_Y, "Up Axis", "");
RNA_def_boolean(ot->srna,
"validate_meshes",
false,

View File

@ -24,6 +24,7 @@
#include "io_cache.h"
#include "io_gpencil.h"
#include "io_obj.h"
#include "io_stl_ops.h"
void ED_operatortypes_io(void)
{
@ -60,4 +61,6 @@ void ED_operatortypes_io(void)
WM_operatortype_append(WM_OT_obj_export);
WM_operatortype_append(WM_OT_obj_import);
WM_operatortype_append(WM_OT_stl_import);
}

View File

@ -0,0 +1,129 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup editor/io
*/
#include "BKE_context.h"
#include "BKE_report.h"
#include "WM_api.h"
#include "WM_types.h"
#include "DNA_space_types.h"
#include "ED_outliner.h"
#include "RNA_access.h"
#include "RNA_define.h"
#include "IO_stl.h"
#include "io_stl_ops.h"
static int wm_stl_import_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
return WM_operator_filesel(C, op, event);
}
static int wm_stl_import_execute(bContext *C, wmOperator *op)
{
struct STLImportParams params;
params.forward_axis = RNA_enum_get(op->ptr, "forward_axis");
params.up_axis = RNA_enum_get(op->ptr, "up_axis");
params.use_facet_normal = RNA_boolean_get(op->ptr, "use_facet_normal");
params.use_scene_unit = RNA_boolean_get(op->ptr, "use_scene_unit");
params.global_scale = RNA_float_get(op->ptr, "global_scale");
params.use_mesh_validate = RNA_boolean_get(op->ptr, "use_mesh_validate");
int files_len = RNA_collection_length(op->ptr, "files");
if (files_len) {
PointerRNA fileptr;
PropertyRNA *prop;
char dir_only[FILE_MAX], file_only[FILE_MAX];
RNA_string_get(op->ptr, "directory", dir_only);
prop = RNA_struct_find_property(op->ptr, "files");
for (int i = 0; i < files_len; i++) {
RNA_property_collection_lookup_int(op->ptr, prop, i, &fileptr);
RNA_string_get(&fileptr, "name", file_only);
BLI_join_dirfile(params.filepath, sizeof(params.filepath), dir_only, file_only);
STL_import(C, &params);
}
}
else if (RNA_struct_property_is_set(op->ptr, "filepath")) {
RNA_string_get(op->ptr, "filepath", params.filepath);
STL_import(C, &params);
}
else {
BKE_report(op->reports, RPT_ERROR, "No filename given");
return OPERATOR_CANCELLED;
}
Scene *scene = CTX_data_scene(C);
WM_event_add_notifier(C, NC_SCENE | ND_OB_SELECT, scene);
WM_event_add_notifier(C, NC_SCENE | ND_OB_ACTIVE, scene);
WM_event_add_notifier(C, NC_SCENE | ND_LAYER_CONTENT, scene);
ED_outliner_select_sync_from_object_tag(C);
return OPERATOR_FINISHED;
}
static bool wm_stl_import_check(bContext *UNUSED(C), wmOperator *op)
{
const int num_axes = 3;
/* Both forward and up axes cannot be the same (or same except opposite sign). */
if (RNA_enum_get(op->ptr, "forward_axis") % num_axes ==
(RNA_enum_get(op->ptr, "up_axis") % num_axes)) {
RNA_enum_set(op->ptr, "up_axis", RNA_enum_get(op->ptr, "up_axis") % num_axes + 1);
return true;
}
return false;
}
void WM_OT_stl_import(struct wmOperatorType *ot)
{
PropertyRNA *prop;
ot->name = "Import STL";
ot->description = "Import an STL file as an object";
ot->idname = "WM_OT_stl_import";
ot->invoke = wm_stl_import_invoke;
ot->exec = wm_stl_import_execute;
ot->poll = WM_operator_winactive;
ot->check = wm_stl_import_check;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
WM_operator_properties_filesel(ot,
FILE_TYPE_FOLDER,
FILE_BLENDER,
FILE_OPENFILE,
WM_FILESEL_FILEPATH | WM_FILESEL_FILES | WM_FILESEL_DIRECTORY |
WM_FILESEL_SHOW_PROPS,
FILE_DEFAULTDISPLAY,
FILE_SORT_ALPHA);
RNA_def_float(ot->srna, "global_scale", 1.0f, 1e-6f, 1e6f, "Scale", "", 0.001f, 1000.0f);
RNA_def_boolean(ot->srna,
"use_scene_unit",
false,
"Scene Unit",
"Apply current scene's unit (as defined by unit scale) to imported data");
RNA_def_boolean(ot->srna,
"use_facet_normal",
false,
"Facet Normals",
"Use (import) facet normals (note that this will still give flat shading)");
RNA_def_enum(ot->srna, "forward_axis", io_transform_axis, IO_AXIS_Y, "Forward Axis", "");
RNA_def_enum(ot->srna, "up_axis", io_transform_axis, IO_AXIS_Z, "Up Axis", "");
RNA_def_boolean(ot->srna,
"use_mesh_validate",
false,
"Validate Mesh",
"Validate and correct imported mesh (slow)");
/* Only show .stl files by default. */
prop = RNA_def_string(ot->srna, "filter_glob", "*.stl", 0, "Extension Filter", "");
RNA_def_property_flag(prop, PROP_HIDDEN);
}

View File

@ -0,0 +1,12 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup editor/io
*/
#pragma once
struct wmOperatorType;
void WM_OT_stl_export(struct wmOperatorType *ot);
void WM_OT_stl_import(struct wmOperatorType *ot);

View File

@ -2803,7 +2803,7 @@ int ED_path_extension_type(const char *path)
return FILE_TYPE_ARCHIVE;
}
if (BLI_path_extension_check_n(
path, ".obj", ".mtl", ".3ds", ".fbx", ".glb", ".gltf", ".svg", NULL)) {
path, ".obj", ".mtl", ".3ds", ".fbx", ".glb", ".gltf", ".svg", ".stl", NULL)) {
return FILE_TYPE_OBJECT_IO;
}
if (BLI_path_extension_check_array(path, imb_ext_image)) {

View File

@ -3,6 +3,7 @@
add_subdirectory(common)
add_subdirectory(wavefront_obj)
add_subdirectory(stl)
if(WITH_ALEMBIC)
add_subdirectory(alembic)

View File

@ -8,6 +8,7 @@ set(INC
../../depsgraph
../../makesdna
../../../../intern/guardedalloc
../../makesrna
)
set(INC_SYS
@ -19,12 +20,14 @@ set(SRC
intern/dupli_persistent_id.cc
intern/object_identifier.cc
intern/path_util.cc
intern/orientation.c
IO_abstract_hierarchy_iterator.h
IO_dupli_persistent_id.hh
IO_path_util.hh
IO_path_util_types.h
IO_types.h
IO_orientation.h
intern/dupli_parent_finder.hh
)

View File

@ -0,0 +1,16 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "RNA_types.h"
typedef enum {
IO_AXIS_X = 0,
IO_AXIS_Y = 1,
IO_AXIS_Z = 2,
IO_AXIS_NEGATIVE_X = 3,
IO_AXIS_NEGATIVE_Y = 4,
IO_AXIS_NEGATIVE_Z = 5,
} eIOAxis;
extern const EnumPropertyItem io_transform_axis[];

View File

@ -0,0 +1,14 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "RNA_types.h"
#include "IO_orientation.h"
const EnumPropertyItem io_transform_axis[] = {
{IO_AXIS_X, "X", 0, "X", "Positive X axis"},
{IO_AXIS_Y, "Y", 0, "Y", "Positive Y axis"},
{IO_AXIS_Z, "Z", 0, "Z", "Positive Z axis"},
{IO_AXIS_NEGATIVE_X, "NEGATIVE_X", 0, "-X", "Negative X axis"},
{IO_AXIS_NEGATIVE_Y, "NEGATIVE_Y", 0, "-Y", "Negative Y axis"},
{IO_AXIS_NEGATIVE_Z, "NEGATIVE_Z", 0, "-Z", "Negative Z axis"},
{0, NULL, 0, NULL, NULL}};

View File

@ -0,0 +1,44 @@
# SPDX-License-Identifier: GPL-2.0-or-later
set(INC
.
./importer
../common
../../blenkernel
../../blenlib
../../bmesh
../../bmesh/intern
../../depsgraph
../../editors/include
../../makesdna
../../makesrna
../../nodes
../../windowmanager
../../../../extern/fast_float
../../../../intern/guardedalloc
)
set(INC_SYS
)
set(SRC
IO_stl.cc
importer/stl_import_mesh.cc
importer/stl_import_ascii_reader.cc
importer/stl_import_binary_reader.cc
importer/stl_import.cc
IO_stl.h
importer/stl_import_mesh.hh
importer/stl_import_ascii_reader.hh
importer/stl_import_binary_reader.hh
importer/stl_import.hh
)
set(LIB
bf_blenkernel
bf_io_common
)
blender_add_lib(bf_stl "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")

View File

@ -0,0 +1,16 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#include "BLI_timeit.hh"
#include "IO_stl.h"
#include "stl_import.hh"
void STL_import(bContext *C, const struct STLImportParams *import_params)
{
SCOPED_TIMER("STL Import");
blender::io::stl::importer_main(C, *import_params);
}

View File

@ -0,0 +1,35 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#pragma once
#include "BKE_context.h"
#include "BLI_path_util.h"
#include "IO_orientation.h"
#ifdef __cplusplus
extern "C" {
#endif
struct STLImportParams {
/** Full path to the source STL file to import. */
char filepath[FILE_MAX];
eIOAxis forward_axis;
eIOAxis up_axis;
bool use_facet_normal;
bool use_scene_unit;
float global_scale;
bool use_mesh_validate;
};
/**
* C-interface for the importer.
*/
void STL_import(bContext *C, const struct STLImportParams *import_params);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,114 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#include <cstdio>
#include "BKE_customdata.h"
#include "BKE_layer.h"
#include "BKE_mesh.h"
#include "BKE_object.h"
#include "DNA_collection_types.h"
#include "DNA_scene_types.h"
#include "BLI_fileops.hh"
#include "BLI_math_vector.h"
#include "BLI_memory_utils.hh"
#include "DNA_object_types.h"
#include "DEG_depsgraph.h"
#include "DEG_depsgraph_build.h"
#include "stl_import.hh"
#include "stl_import_ascii_reader.hh"
#include "stl_import_binary_reader.hh"
namespace blender::io::stl {
void importer_main(bContext *C, const STLImportParams &import_params)
{
Main *bmain = CTX_data_main(C);
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
importer_main(bmain, scene, view_layer, import_params);
}
void importer_main(Main *bmain,
Scene *scene,
ViewLayer *view_layer,
const STLImportParams &import_params)
{
FILE *file = BLI_fopen(import_params.filepath, "rb");
if (!file) {
fprintf(stderr, "Failed to open STL file:'%s'.\n", import_params.filepath);
return;
}
BLI_SCOPED_DEFER([&]() { fclose(file); });
/* Detect STL file type by comparing file size with expected file size,
* could check if file starts with "solid", but some files do not adhere,
* this is the same as the old Python importer.
*/
uint32_t num_tri = 0;
size_t file_size = BLI_file_size(import_params.filepath);
fseek(file, BINARY_HEADER_SIZE, SEEK_SET);
fread(&num_tri, sizeof(uint32_t), 1, file);
bool is_ascii_stl = (file_size != (BINARY_HEADER_SIZE + 4 + BINARY_STRIDE * num_tri));
/* Name used for both mesh and object. */
char ob_name[FILE_MAX];
BLI_strncpy(ob_name, BLI_path_basename(import_params.filepath), FILE_MAX);
BLI_path_extension_replace(ob_name, FILE_MAX, "");
Mesh *mesh;
if (is_ascii_stl) {
mesh = read_stl_ascii(import_params.filepath, bmain, ob_name, import_params.use_facet_normal);
}
else {
mesh = read_stl_binary(file, bmain, ob_name, import_params.use_facet_normal);
}
if (import_params.use_mesh_validate) {
bool verbose_validate = false;
#ifdef DEBUG
verbose_validate = true;
#endif
BKE_mesh_validate(mesh, verbose_validate, false);
}
BKE_view_layer_base_deselect_all(view_layer);
LayerCollection *lc = BKE_layer_collection_get_active(view_layer);
Object *obj = BKE_object_add_only_object(bmain, OB_MESH, ob_name);
BKE_mesh_assign_object(bmain, obj, mesh);
BKE_collection_object_add(bmain, lc->collection, obj);
Base *base = BKE_view_layer_base_find(view_layer, obj);
BKE_view_layer_base_select_and_set_active(view_layer, base);
float global_scale = import_params.global_scale;
if ((scene->unit.system != USER_UNIT_NONE) && import_params.use_scene_unit) {
global_scale *= scene->unit.scale_length;
}
float scale_vec[3] = {global_scale, global_scale, global_scale};
float obmat3x3[3][3];
unit_m3(obmat3x3);
float obmat4x4[4][4];
unit_m4(obmat4x4);
/* +Y-forward and +Z-up are the Blender's default axis settings. */
mat3_from_axis_conversion(
IO_AXIS_Y, IO_AXIS_Z, import_params.forward_axis, import_params.up_axis, obmat3x3);
copy_m4_m3(obmat4x4, obmat3x3);
rescale_m4(obmat4x4, scale_vec);
BKE_object_apply_mat4(obj, obmat4x4, true, false);
DEG_id_tag_update(&lc->collection->id, ID_RECALC_COPY_ON_WRITE);
int flags = ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY | ID_RECALC_ANIMATION |
ID_RECALC_BASE_FLAGS;
DEG_id_tag_update_ex(bmain, &obj->id, flags);
DEG_id_tag_update(&scene->id, ID_RECALC_BASE_FLAGS);
DEG_relations_tag_update(bmain);
}
} // namespace blender::io::stl

View File

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

View File

@ -0,0 +1,159 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#include <cstdint>
#include <cstdio>
#include "BKE_mesh.h"
#include "BLI_fileops.hh"
#include "BLI_memory_utils.hh"
#include "BLI_string_ref.hh"
#include "DNA_mesh_types.h"
/* NOTE: we could use C++17 <charconv> from_chars to parse
* floats, but even if some compilers claim full support,
* their standard libraries are not quite there yet.
* LLVM/libc++ only has a float parser since LLVM 14,
* and gcc/libstdc++ since 11.1. So until at least these are
* the minimum spec, use an external library. */
#include "fast_float.h"
#include "stl_import.hh"
#include "stl_import_mesh.hh"
namespace blender::io::stl {
class StringBuffer {
private:
char *start;
const char *end;
public:
StringBuffer(char *buf, size_t len)
{
start = buf;
end = start + len;
}
bool is_empty() const
{
return start == end;
}
void drop_leading_control_chars()
{
while ((start < end) && (*start) <= ' ') {
start++;
}
}
void drop_leading_non_control_chars()
{
while ((start < end) && (*start) > ' ') {
start++;
}
}
void drop_line()
{
while (start < end && *start != '\n') {
start++;
}
}
bool parse_token(const char *token, size_t token_length)
{
drop_leading_control_chars();
if (end - start < token_length + 1) {
return false;
}
if (memcmp(start, token, token_length) != 0) {
return false;
}
if (start[token_length] > ' ') {
return false;
}
start += token_length + 1;
return true;
}
void drop_token()
{
drop_leading_non_control_chars();
drop_leading_control_chars();
}
void parse_float(float &out)
{
drop_leading_control_chars();
/* Skip '+' */
if (start < end && *start == '+') {
start++;
}
fast_float::from_chars_result res = fast_float::from_chars(start, end, out);
if (res.ec == std::errc::invalid_argument || res.ec == std::errc::result_out_of_range) {
out = 0.0f;
}
start = const_cast<char *>(res.ptr);
}
};
static inline void parse_float3(StringBuffer &buf, float out[3])
{
for (int i = 0; i < 3; i++) {
buf.parse_float(out[i]);
}
}
Mesh *read_stl_ascii(const char *filepath, Main *bmain, char *mesh_name, bool use_custom_normals)
{
size_t buffer_len;
void *buffer = BLI_file_read_text_as_mem(filepath, 0, &buffer_len);
if (buffer == nullptr) {
fprintf(stderr, "STL Importer: cannot read from ASCII STL file: '%s'\n", filepath);
return BKE_mesh_add(bmain, mesh_name);
}
BLI_SCOPED_DEFER([&]() { MEM_freeN(buffer); });
int num_reserved_tris = 1024;
StringBuffer str_buf(static_cast<char *>(buffer), buffer_len);
STLMeshHelper stl_mesh(num_reserved_tris, use_custom_normals);
float triangle_buf[3][3];
float custom_normal_buf[3];
str_buf.drop_line(); /* Skip header line */
while (!str_buf.is_empty()) {
if (str_buf.parse_token("vertex", 6)) {
parse_float3(str_buf, triangle_buf[0]);
if (str_buf.parse_token("vertex", 6)) {
parse_float3(str_buf, triangle_buf[1]);
}
if (str_buf.parse_token("vertex", 6)) {
parse_float3(str_buf, triangle_buf[2]);
}
if (use_custom_normals) {
stl_mesh.add_triangle(
triangle_buf[0], triangle_buf[1], triangle_buf[2], custom_normal_buf);
}
else {
stl_mesh.add_triangle(triangle_buf[0], triangle_buf[1], triangle_buf[2]);
}
}
else if (str_buf.parse_token("facet", 5)) {
str_buf.drop_token(); /* Expecting "normal" */
parse_float3(str_buf, custom_normal_buf);
}
else {
str_buf.drop_token();
}
}
return stl_mesh.to_mesh(bmain, mesh_name);
}
} // namespace blender::io::stl

View File

@ -0,0 +1,32 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#pragma once
#include <cstdio>
#include "BKE_mesh.h"
#include "stl_import.hh"
/* ASCII STL spec.:
* solid name
* facet normal ni nj nk
* outer loop
* vertex v1x v1y v1z
* vertex v2x v2y v2z
* vertex v3x v3y v3z
* endloop
* endfacet
* ...
* endsolid name
*/
namespace blender::io::stl {
Mesh *read_stl_ascii(const char *filepath, Main *bmain, char *mesh_name, bool use_custom_normals);
} // namespace blender::io::stl

View File

@ -0,0 +1,58 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#include <cstdint>
#include <cstdio>
#include "BKE_main.h"
#include "BKE_mesh.h"
#include "BLI_array.hh"
#include "BLI_memory_utils.hh"
#include "DNA_mesh_types.h"
#include "stl_import_binary_reader.hh"
#include "stl_import_mesh.hh"
namespace blender::io::stl {
#pragma pack(push, 1)
struct STLBinaryTriangle {
float normal[3];
float v1[3], v2[3], v3[3];
uint16_t attribute_byte_count;
};
#pragma pack(pop)
Mesh *read_stl_binary(FILE *file, Main *bmain, char *mesh_name, bool use_custom_normals)
{
const int chunk_size = 1024;
uint32_t num_tris = 0;
fseek(file, BINARY_HEADER_SIZE, SEEK_SET);
fread(&num_tris, sizeof(uint32_t), 1, file);
if (num_tris == 0) {
return BKE_mesh_add(bmain, mesh_name);
}
Array<STLBinaryTriangle> tris_buf(chunk_size);
STLMeshHelper stl_mesh(num_tris, use_custom_normals);
size_t num_read_tris;
while (num_read_tris = fread(tris_buf.data(), sizeof(STLBinaryTriangle), chunk_size, file)) {
for (size_t i = 0; i < num_read_tris; i++) {
if (use_custom_normals) {
stl_mesh.add_triangle(tris_buf[i].v1, tris_buf[i].v2, tris_buf[i].v3, tris_buf[i].normal);
}
else {
stl_mesh.add_triangle(tris_buf[i].v1, tris_buf[i].v2, tris_buf[i].v3);
}
}
}
return stl_mesh.to_mesh(bmain, mesh_name);
}
} // namespace blender::io::stl

View File

@ -0,0 +1,31 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#pragma once
#include <cstdio>
#include "BKE_mesh.h"
/* Binary STL spec.:
* UINT8[80] Header - 80 bytes
* UINT32 Number of triangles - 4 bytes
* For each triangle - 50 bytes:
* REAL32[3] Normal vector - 12 bytes
* REAL32[3] Vertex 1 - 12 bytes
* REAL32[3] Vertex 2 - 12 bytes
* REAL32[3] Vertex 3 - 12 bytes
* UINT16 Attribute byte count - 2 bytes
*/
namespace blender::io::stl {
const size_t BINARY_HEADER_SIZE = 80;
const size_t BINARY_STRIDE = 12 * 4 + 2;
Mesh *read_stl_binary(FILE *file, Main *bmain, char *mesh_name, bool use_custom_normals);
} // namespace blender::io::stl

View File

@ -0,0 +1,114 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#include "BKE_customdata.h"
#include "BKE_lib_id.h"
#include "BKE_main.h"
#include "BKE_mesh.h"
#include "BLI_array.hh"
#include "BLI_math_vector.h"
#include "BLI_math_vector.hh"
#include "BLI_task.hh"
#include "DNA_mesh_types.h"
#include "DNA_meshdata_types.h"
#include "stl_import_mesh.hh"
namespace blender::io::stl {
STLMeshHelper::STLMeshHelper(int num_tris, bool use_custom_normals)
: m_use_custom_normals(use_custom_normals)
{
m_num_degenerate_tris = 0;
m_num_duplicate_tris = 0;
m_tris.reserve(num_tris);
/* Upper bound (all vertices are unique). */
m_verts.reserve(num_tris * 3);
if (use_custom_normals) {
m_loop_normals.reserve(num_tris * 3);
}
}
bool STLMeshHelper::add_triangle(const float3 &a, const float3 &b, const float3 &c)
{
int v1_id = m_verts.index_of_or_add(a);
int v2_id = m_verts.index_of_or_add(b);
int v3_id = m_verts.index_of_or_add(c);
if ((v1_id == v2_id) || (v1_id == v3_id) || (v2_id == v3_id)) {
m_num_degenerate_tris++;
return false;
}
if (!m_tris.add({v1_id, v2_id, v3_id})) {
m_num_duplicate_tris++;
return false;
}
return true;
}
void STLMeshHelper::add_triangle(const float3 &a,
const float3 &b,
const float3 &c,
const float3 &custom_normal)
{
if (add_triangle(a, b, c)) {
m_loop_normals.append_n_times(custom_normal, 3);
}
}
Mesh *STLMeshHelper::to_mesh(Main *bmain, char *mesh_name)
{
if (m_num_degenerate_tris > 0) {
std::cout << "STL Importer: " << m_num_degenerate_tris << "degenerate triangles were removed"
<< std::endl;
}
if (m_num_duplicate_tris > 0) {
std::cout << "STL Importer: " << m_num_duplicate_tris << "duplicate triangles were removed"
<< std::endl;
}
Mesh *mesh = BKE_mesh_add(bmain, mesh_name);
/* User count is already 1 here, but will be set later in #BKE_mesh_assign_object. */
id_us_min(&mesh->id);
mesh->totvert = m_verts.size();
mesh->mvert = static_cast<MVert *>(
CustomData_add_layer(&mesh->vdata, CD_MVERT, CD_CALLOC, nullptr, mesh->totvert));
for (int i = 0; i < mesh->totvert; i++) {
copy_v3_v3(mesh->mvert[i].co, m_verts[i]);
}
mesh->totpoly = m_tris.size();
mesh->totloop = m_tris.size() * 3;
mesh->mpoly = static_cast<MPoly *>(
CustomData_add_layer(&mesh->pdata, CD_MPOLY, CD_CALLOC, nullptr, mesh->totpoly));
mesh->mloop = static_cast<MLoop *>(
CustomData_add_layer(&mesh->ldata, CD_MLOOP, CD_CALLOC, nullptr, mesh->totloop));
threading::parallel_for(m_tris.index_range(), 2048, [&](IndexRange tris_range) {
for (const int i : tris_range) {
mesh->mpoly[i].loopstart = 3 * i;
mesh->mpoly[i].totloop = 3;
mesh->mloop[3 * i].v = m_tris[i].v1;
mesh->mloop[3 * i + 1].v = m_tris[i].v2;
mesh->mloop[3 * i + 2].v = m_tris[i].v3;
}
});
/* NOTE: edges must be calculated first before setting custom normals. */
BKE_mesh_calc_edges(mesh, false, false);
if (m_use_custom_normals && m_loop_normals.size() == mesh->totloop) {
BKE_mesh_set_custom_normals(mesh, reinterpret_cast<float(*)[3]>(m_loop_normals.data()));
mesh->flag |= ME_AUTOSMOOTH;
}
return mesh;
}
} // namespace blender::io::stl

View File

@ -0,0 +1,71 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#pragma once
#include <cstdint>
#include "BLI_math_vec_types.hh"
#include "BLI_set.hh"
#include "BLI_vector.hh"
#include "BLI_vector_set.hh"
#include "DNA_mesh_types.h"
namespace blender::io::stl {
class Triangle {
public:
int v1, v2, v3;
/* Based on an old version of Python's frozenset hash
* https://web.archive.org/web/20220520211017/https://stackoverflow.com/questions/20832279/python-frozenset-hashing-algorithm-implementation
*/
uint64_t hash() const
{
uint64_t res = 1927868237UL;
res *= 4;
res ^= (v1 ^ (v1 << 16) ^ 89869747UL) * 3644798167UL;
res ^= (v2 ^ (v2 << 16) ^ 89869747UL) * 3644798167UL;
res ^= (v3 ^ (v3 << 16) ^ 89869747UL) * 3644798167UL;
return res * 69069U + 907133923UL;
}
friend bool operator==(const Triangle &a, const Triangle &b)
{
bool i = (a.v1 == b.v1) && (a.v2 == b.v2) && (a.v3 == b.v3);
bool j = (a.v1 == b.v1) && (a.v3 == b.v2) && (a.v2 == b.v3);
bool k = (a.v2 == b.v1) && (a.v1 == b.v2) && (a.v3 == b.v3);
bool l = (a.v2 == b.v1) && (a.v3 == b.v2) && (a.v1 == b.v3);
bool m = (a.v3 == b.v1) && (a.v1 == b.v2) && (a.v2 == b.v3);
bool n = (a.v3 == b.v1) && (a.v2 == b.v2) && (a.v1 == b.v3);
return i || j || k || l || m || n;
}
};
class STLMeshHelper {
private:
VectorSet<float3> m_verts;
VectorSet<Triangle> m_tris;
Vector<float3> m_loop_normals;
int m_num_degenerate_tris;
int m_num_duplicate_tris;
const bool m_use_custom_normals;
public:
STLMeshHelper(int num_tris, bool use_custom_normals);
/* Creates a new triangle from specified vertex locations,
* duplicate vertices and triangles are merged.
*/
bool add_triangle(const float3 &a, const float3 &b, const float3 &c);
void add_triangle(const float3 &a,
const float3 &b,
const float3 &c,
const float3 &custom_normal);
Mesh *to_mesh(Main *bmain, char *mesh_name);
};
} // namespace blender::io::stl

View File

@ -9,30 +9,13 @@
#include "BKE_context.h"
#include "BLI_path_util.h"
#include "DEG_depsgraph.h"
#include "IO_orientation.h"
#include "IO_path_util_types.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
OBJ_AXIS_X_UP = 0,
OBJ_AXIS_Y_UP = 1,
OBJ_AXIS_Z_UP = 2,
OBJ_AXIS_NEGATIVE_X_UP = 3,
OBJ_AXIS_NEGATIVE_Y_UP = 4,
OBJ_AXIS_NEGATIVE_Z_UP = 5,
} eTransformAxisUp;
typedef enum {
OBJ_AXIS_X_FORWARD = 0,
OBJ_AXIS_Y_FORWARD = 1,
OBJ_AXIS_Z_FORWARD = 2,
OBJ_AXIS_NEGATIVE_X_FORWARD = 3,
OBJ_AXIS_NEGATIVE_Y_FORWARD = 4,
OBJ_AXIS_NEGATIVE_Z_FORWARD = 5,
} eTransformAxisForward;
static const int TOTAL_AXES = 3;
struct OBJExportParams {
@ -52,8 +35,8 @@ struct OBJExportParams {
int end_frame;
/* Geometry Transform options. */
eTransformAxisForward forward_axis;
eTransformAxisUp up_axis;
eIOAxis forward_axis;
eIOAxis up_axis;
float scaling_factor;
/* File Write Options. */
@ -86,8 +69,8 @@ struct OBJImportParams {
char filepath[FILE_MAX];
/** Value 0 disables clamping. */
float clamp_size;
eTransformAxisForward forward_axis;
eTransformAxisUp up_axis;
eIOAxis forward_axis;
eIOAxis up_axis;
bool validate_meshes;
};

View File

@ -117,13 +117,12 @@ std::pair<Mesh *, bool> OBJMesh::triangulate_mesh_eval()
return {triangulated, true};
}
void OBJMesh::set_world_axes_transform(const eTransformAxisForward forward,
const eTransformAxisUp up)
void OBJMesh::set_world_axes_transform(const eIOAxis forward, const eIOAxis up)
{
float axes_transform[3][3];
unit_m3(axes_transform);
/* +Y-forward and +Z-up are the default Blender axis settings. */
mat3_from_axis_conversion(OBJ_AXIS_Y_FORWARD, OBJ_AXIS_Z_UP, forward, up, axes_transform);
mat3_from_axis_conversion(IO_AXIS_Y, IO_AXIS_Z, forward, up, axes_transform);
/* mat3_from_axis_conversion returns a transposed matrix! */
transpose_m3(axes_transform);
mul_m4_m3m4(world_and_axes_transform_, axes_transform, export_object_eval_.obmat);

View File

@ -256,6 +256,6 @@ class OBJMesh : NonCopyable {
/**
* Set the final transform after applying axes settings and an Object's world transform.
*/
void set_world_axes_transform(eTransformAxisForward forward, eTransformAxisUp up);
void set_world_axes_transform(eIOAxis forward, eIOAxis up);
};
} // namespace blender::io::obj

View File

@ -25,13 +25,12 @@ OBJCurve::OBJCurve(const Depsgraph *depsgraph,
set_world_axes_transform(export_params.forward_axis, export_params.up_axis);
}
void OBJCurve::set_world_axes_transform(const eTransformAxisForward forward,
const eTransformAxisUp up)
void OBJCurve::set_world_axes_transform(const eIOAxis forward, const eIOAxis up)
{
float axes_transform[3][3];
unit_m3(axes_transform);
/* +Y-forward and +Z-up are the Blender's default axis settings. */
mat3_from_axis_conversion(OBJ_AXIS_Y_FORWARD, OBJ_AXIS_Z_UP, forward, up, axes_transform);
mat3_from_axis_conversion(IO_AXIS_Y, IO_AXIS_Z, forward, up, axes_transform);
/* mat3_from_axis_conversion returns a transposed matrix! */
transpose_m3(axes_transform);
mul_m4_m3m4(world_axes_transform_, axes_transform, export_object_eval_->obmat);

View File

@ -56,7 +56,7 @@ class OBJCurve : NonCopyable {
/**
* Set the final transform after applying axes settings and an Object's world transform.
*/
void set_world_axes_transform(eTransformAxisForward forward, eTransformAxisUp up);
void set_world_axes_transform(eIOAxis forward, eIOAxis up);
};
} // namespace blender::io::obj

View File

@ -99,11 +99,8 @@ void transform_object(Object *object, const OBJImportParams &import_params)
float obmat[4][4];
unit_m4(obmat);
/* +Y-forward and +Z-up are the default Blender axis settings. */
mat3_from_axis_conversion(import_params.forward_axis,
import_params.up_axis,
OBJ_AXIS_Y_FORWARD,
OBJ_AXIS_Z_UP,
axes_transform);
mat3_from_axis_conversion(
import_params.forward_axis, import_params.up_axis, IO_AXIS_Y, IO_AXIS_Z, axes_transform);
/* mat3_from_axis_conversion returns a transposed matrix! */
transpose_m3(axes_transform);
copy_m4_m3(obmat, axes_transform);

View File

@ -315,8 +315,8 @@ TEST_F(obj_exporter_regression_test, all_quads)
TEST_F(obj_exporter_regression_test, fgons)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.forward_axis = IO_AXIS_Y;
_export.params.up_axis = IO_AXIS_Z;
_export.params.export_materials = false;
compare_obj_export_to_golden(
"io_tests/blend_geometry/fgons.blend", "io_tests/obj/fgons.obj", "", _export.params);
@ -325,8 +325,8 @@ TEST_F(obj_exporter_regression_test, fgons)
TEST_F(obj_exporter_regression_test, edges)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.forward_axis = IO_AXIS_Y;
_export.params.up_axis = IO_AXIS_Z;
_export.params.export_materials = false;
compare_obj_export_to_golden(
"io_tests/blend_geometry/edges.blend", "io_tests/obj/edges.obj", "", _export.params);
@ -335,8 +335,8 @@ TEST_F(obj_exporter_regression_test, edges)
TEST_F(obj_exporter_regression_test, vertices)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.forward_axis = IO_AXIS_Y;
_export.params.up_axis = IO_AXIS_Z;
_export.params.export_materials = false;
compare_obj_export_to_golden(
"io_tests/blend_geometry/vertices.blend", "io_tests/obj/vertices.obj", "", _export.params);
@ -355,8 +355,8 @@ TEST_F(obj_exporter_regression_test, non_uniform_scale)
TEST_F(obj_exporter_regression_test, nurbs_as_nurbs)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.forward_axis = IO_AXIS_Y;
_export.params.up_axis = IO_AXIS_Z;
_export.params.export_materials = false;
_export.params.export_curves_as_nurbs = true;
compare_obj_export_to_golden(
@ -366,8 +366,8 @@ TEST_F(obj_exporter_regression_test, nurbs_as_nurbs)
TEST_F(obj_exporter_regression_test, nurbs_curves_as_nurbs)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.forward_axis = IO_AXIS_Y;
_export.params.up_axis = IO_AXIS_Z;
_export.params.export_materials = false;
_export.params.export_curves_as_nurbs = true;
compare_obj_export_to_golden("io_tests/blend_geometry/nurbs_curves.blend",
@ -379,8 +379,8 @@ TEST_F(obj_exporter_regression_test, nurbs_curves_as_nurbs)
TEST_F(obj_exporter_regression_test, nurbs_as_mesh)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.forward_axis = IO_AXIS_Y;
_export.params.up_axis = IO_AXIS_Z;
_export.params.export_materials = false;
_export.params.export_curves_as_nurbs = false;
compare_obj_export_to_golden(
@ -390,8 +390,8 @@ TEST_F(obj_exporter_regression_test, nurbs_as_mesh)
TEST_F(obj_exporter_regression_test, cube_all_data_triangulated)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.forward_axis = IO_AXIS_Y;
_export.params.up_axis = IO_AXIS_Z;
_export.params.export_materials = false;
_export.params.export_triangulated_mesh = true;
compare_obj_export_to_golden("io_tests/blend_geometry/cube_all_data.blend",
@ -403,8 +403,8 @@ TEST_F(obj_exporter_regression_test, cube_all_data_triangulated)
TEST_F(obj_exporter_regression_test, cube_normal_edit)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.forward_axis = IO_AXIS_Y;
_export.params.up_axis = IO_AXIS_Z;
_export.params.export_materials = false;
compare_obj_export_to_golden("io_tests/blend_geometry/cube_normal_edit.blend",
"io_tests/obj/cube_normal_edit.obj",
@ -459,8 +459,8 @@ TEST_F(obj_exporter_regression_test, cubes_with_textures_relative)
TEST_F(obj_exporter_regression_test, suzanne_all_data)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.forward_axis = IO_AXIS_Y;
_export.params.up_axis = IO_AXIS_Z;
_export.params.export_materials = false;
_export.params.export_smooth_groups = true;
compare_obj_export_to_golden("io_tests/blend_geometry/suzanne_all_data.blend",
@ -491,8 +491,8 @@ TEST_F(obj_exporter_regression_test, all_curves_as_nurbs)
TEST_F(obj_exporter_regression_test, all_objects)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.forward_axis = IO_AXIS_Y;
_export.params.up_axis = IO_AXIS_Z;
_export.params.export_smooth_groups = true;
compare_obj_export_to_golden("io_tests/blend_scene/all_objects.blend",
"io_tests/obj/all_objects.obj",
@ -503,8 +503,8 @@ TEST_F(obj_exporter_regression_test, all_objects)
TEST_F(obj_exporter_regression_test, all_objects_mat_groups)
{
OBJExportParamsDefault _export;
_export.params.forward_axis = OBJ_AXIS_Y_FORWARD;
_export.params.up_axis = OBJ_AXIS_Z_UP;
_export.params.forward_axis = IO_AXIS_Y;
_export.params.up_axis = IO_AXIS_Z;
_export.params.export_smooth_groups = true;
_export.params.export_material_groups = true;
compare_obj_export_to_golden("io_tests/blend_scene/all_objects.blend",

View File

@ -17,8 +17,8 @@ struct OBJExportParamsDefault {
params.start_frame = 0;
params.end_frame = 1;
params.forward_axis = OBJ_AXIS_NEGATIVE_Z_FORWARD;
params.up_axis = OBJ_AXIS_Y_UP;
params.forward_axis = IO_AXIS_NEGATIVE_Z;
params.up_axis = IO_AXIS_Y;
params.scaling_factor = 1.f;
params.apply_modifiers = true;

View File

@ -55,8 +55,8 @@ class obj_importer_test : public BlendfileLoadingBaseTest {
OBJImportParams params;
params.clamp_size = 0;
params.forward_axis = OBJ_AXIS_NEGATIVE_Z_FORWARD;
params.up_axis = OBJ_AXIS_Y_UP;
params.forward_axis = IO_AXIS_NEGATIVE_Z;
params.up_axis = IO_AXIS_Y;
std::string obj_path = blender::tests::flags_test_asset_dir() + "/io_tests/obj/" + path;
strncpy(params.filepath, obj_path.c_str(), FILE_MAX - 1);