IO: C++ STL exporter

There was a C++ STL importer since Blender 3.3, but no corresponding C++ STL exporter. This PR is adding said exporter: taking https://projects.blender.org/blender/blender/pulls/105598 and finishing it (agreed with original author).

Exporting Suzanne with 6 level subdivision (4 million triangles), on Apple M1 Max:
- Binary: python exporter 7.8 sec -> C++ exporter 0.9 sec.
- Ascii: python exporter 13.1 sec -> C++ exporter 4.5 sec.

Co-authored-by: Iyad Ahmed <iyadahmed430@gmail.com>
Pull Request: https://projects.blender.org/blender/blender/pulls/114862
This commit is contained in:
Aras Pranckevicius 2023-11-19 16:41:20 +01:00 committed by Aras Pranckevicius
parent 31abb2b3af
commit 17c793e43c
10 changed files with 445 additions and 3 deletions

View File

@ -514,6 +514,8 @@ class TOPBAR_MT_file_export(Menu):
self.layout.operator("wm.obj_export", text="Wavefront (.obj)")
if bpy.app.build_options.io_ply:
self.layout.operator("wm.ply_export", text="Stanford PLY (.ply)")
if bpy.app.build_options.io_stl:
self.layout.operator("wm.stl_export", text="STL (.stl) (experimental)")
class TOPBAR_MT_file_external_data(Menu):

View File

@ -72,5 +72,6 @@ void ED_operatortypes_io()
#ifdef WITH_IO_STL
WM_operatortype_append(WM_OT_stl_import);
WM_operatortype_append(WM_OT_stl_export);
#endif
}

View File

@ -16,14 +16,164 @@
# include "DNA_space_types.h"
# include "ED_fileselect.hh"
# include "ED_outliner.hh"
# include "RNA_access.hh"
# include "RNA_define.hh"
# include "BLT_translation.h"
# include "UI_interface.hh"
# include "UI_resources.hh"
# include "IO_stl.hh"
# include "io_stl_ops.hh"
static int wm_stl_export_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/)
{
ED_fileselect_ensure_default_filepath(C, op, ".stl");
WM_event_add_fileselect(C, op);
return OPERATOR_RUNNING_MODAL;
}
static int wm_stl_export_execute(bContext *C, wmOperator *op)
{
if (!RNA_struct_property_is_set_ex(op->ptr, "filepath", false)) {
BKE_report(op->reports, RPT_ERROR, "No filename given");
return OPERATOR_CANCELLED;
}
struct STLExportParams export_params;
RNA_string_get(op->ptr, "filepath", export_params.filepath);
export_params.forward_axis = eIOAxis(RNA_enum_get(op->ptr, "forward_axis"));
export_params.up_axis = eIOAxis(RNA_enum_get(op->ptr, "up_axis"));
export_params.global_scale = RNA_float_get(op->ptr, "global_scale");
export_params.apply_modifiers = RNA_boolean_get(op->ptr, "apply_modifiers");
export_params.export_selected_objects = RNA_boolean_get(op->ptr, "export_selected_objects");
export_params.ascii_format = RNA_boolean_get(op->ptr, "ascii_format");
export_params.use_batch = RNA_boolean_get(op->ptr, "use_batch");
STL_export(C, &export_params);
return OPERATOR_FINISHED;
}
static void ui_stl_export_settings(uiLayout *layout, PointerRNA *op_props_ptr)
{
uiLayoutSetPropSep(layout, true);
uiLayoutSetPropDecorate(layout, false);
uiLayout *box, *col, *sub;
box = uiLayoutBox(layout);
col = uiLayoutColumn(box, false);
uiItemR(col, op_props_ptr, "ascii_format", UI_ITEM_NONE, IFACE_("ASCII"), ICON_NONE);
uiItemR(col, op_props_ptr, "use_batch", UI_ITEM_NONE, IFACE_("Batch"), ICON_NONE);
box = uiLayoutBox(layout);
sub = uiLayoutColumnWithHeading(box, false, IFACE_("Include"));
uiItemR(sub,
op_props_ptr,
"export_selected_objects",
UI_ITEM_NONE,
IFACE_("Selection Only"),
ICON_NONE);
box = uiLayoutBox(layout);
sub = uiLayoutColumnWithHeading(box, false, IFACE_("Transform"));
uiItemR(sub, op_props_ptr, "global_scale", UI_ITEM_NONE, IFACE_("Scale"), ICON_NONE);
uiItemR(sub, op_props_ptr, "use_scene_unit", UI_ITEM_NONE, IFACE_("Scene Unit"), ICON_NONE);
uiItemR(sub, op_props_ptr, "forward_axis", UI_ITEM_NONE, IFACE_("Forward"), ICON_NONE);
uiItemR(sub, op_props_ptr, "up_axis", UI_ITEM_NONE, IFACE_("Up"), ICON_NONE);
box = uiLayoutBox(layout);
sub = uiLayoutColumnWithHeading(box, false, IFACE_("Geometry"));
uiItemR(
sub, op_props_ptr, "apply_modifiers", UI_ITEM_NONE, IFACE_("Apply Modifiers"), ICON_NONE);
}
static void wm_stl_export_draw(bContext * /*C*/, wmOperator *op)
{
PointerRNA ptr = RNA_pointer_create(nullptr, op->type->srna, op->properties);
ui_stl_export_settings(op->layout, &ptr);
}
/**
* Return true if any property in the UI is changed.
*/
static bool wm_stl_export_check(bContext * /*C*/, wmOperator *op)
{
char filepath[FILE_MAX];
bool changed = false;
RNA_string_get(op->ptr, "filepath", filepath);
if (!BLI_path_extension_check(filepath, ".stl")) {
BLI_path_extension_ensure(filepath, FILE_MAX, ".stl");
RNA_string_set(op->ptr, "filepath", filepath);
changed = true;
}
return changed;
}
void WM_OT_stl_export(wmOperatorType *ot)
{
PropertyRNA *prop;
ot->name = "Export STL";
ot->description = "Save the scene to an STL file";
ot->idname = "WM_OT_stl_export";
ot->invoke = wm_stl_export_invoke;
ot->exec = wm_stl_export_execute;
ot->poll = WM_operator_winactive;
ot->ui = wm_stl_export_draw;
ot->check = wm_stl_export_check;
ot->flag = OPTYPE_PRESET;
WM_operator_properties_filesel(ot,
FILE_TYPE_FOLDER,
FILE_BLENDER,
FILE_SAVE,
WM_FILESEL_FILEPATH | WM_FILESEL_SHOW_PROPS,
FILE_DEFAULTDISPLAY,
FILE_SORT_DEFAULT);
RNA_def_boolean(ot->srna,
"ascii_format",
false,
"ASCII Format",
"Export file in ASCII format, export as binary otherwise");
RNA_def_boolean(
ot->srna, "use_batch", false, "Batch Export", "Export each object to a separate file");
RNA_def_boolean(ot->srna,
"export_selected_objects",
false,
"Export Selected Objects",
"Export only selected objects instead of all supported objects");
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 exported data");
prop = RNA_def_enum(ot->srna, "forward_axis", io_transform_axis, IO_AXIS_Y, "Forward Axis", "");
RNA_def_property_update_runtime(prop, io_ui_forward_axis_update);
prop = RNA_def_enum(ot->srna, "up_axis", io_transform_axis, IO_AXIS_Z, "Up Axis", "");
RNA_def_property_update_runtime(prop, io_ui_up_axis_update);
RNA_def_boolean(
ot->srna, "apply_modifiers", true, "Apply Modifiers", "Apply modifiers to exported meshes");
/* 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);
}
static int wm_stl_import_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
return WM_operator_filesel(C, op, event);

View File

@ -5,6 +5,7 @@
set(INC
.
importer
exporter
../common
../../blenkernel
../../bmesh
@ -18,6 +19,7 @@ set(INC
set(INC_SYS
../../../../extern/fast_float
../../../../extern/fmtlib/include
)
set(SRC
@ -26,12 +28,16 @@ set(SRC
importer/stl_import_ascii_reader.cc
importer/stl_import_binary_reader.cc
importer/stl_import_mesh.cc
exporter/stl_export.cc
exporter/stl_export_writer.cc
IO_stl.hh
importer/stl_import.hh
importer/stl_import_ascii_reader.hh
importer/stl_import_binary_reader.hh
importer/stl_import_mesh.hh
exporter/stl_export_writer.hh
exporter/stl_export.hh
)
set(LIB
@ -40,6 +46,7 @@ set(LIB
PRIVATE bf::dna
PRIVATE bf::intern::guardedalloc
bf_io_common
extern_fmtlib
)
blender_add_lib(bf_io_stl "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")

View File

@ -9,6 +9,7 @@
#include "BLI_timeit.hh"
#include "IO_stl.hh"
#include "stl_export.hh"
#include "stl_import.hh"
void STL_import(bContext *C, const STLImportParams *import_params)
@ -16,3 +17,9 @@ void STL_import(bContext *C, const STLImportParams *import_params)
SCOPED_TIMER("STL Import");
blender::io::stl::importer_main(C, *import_params);
}
void STL_export(bContext *C, const STLExportParams *export_params)
{
SCOPED_TIMER("STL Export");
blender::io::stl::exporter_main(C, *export_params);
}

View File

@ -23,7 +23,18 @@ struct STLImportParams {
bool use_mesh_validate;
};
/**
* C-interface for the importer.
*/
struct STLExportParams {
/** Full path to the to-be-saved STL file. */
char filepath[FILE_MAX];
eIOAxis forward_axis;
eIOAxis up_axis;
float global_scale;
bool export_selected_objects;
bool use_scene_unit;
bool apply_modifiers;
bool ascii_format;
bool use_batch;
};
void STL_import(bContext *C, const STLImportParams *import_params);
void STL_export(bContext *C, const STLExportParams *export_params);

View File

@ -0,0 +1,113 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#include <algorithm>
#include <memory>
#include <string>
#include "BKE_mesh.hh"
#include "BKE_object.hh"
#include "BLI_string.h"
#include "DEG_depsgraph_query.hh"
#include "DNA_scene_types.h"
#include "BLI_math_matrix.h"
#include "BLI_math_rotation.h"
#include "BLI_math_vector.h"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "IO_stl.hh"
#include "stl_export.hh"
#include "stl_export_writer.hh"
namespace blender::io::stl {
void exporter_main(bContext *C, const STLExportParams &export_params)
{
std::unique_ptr<FileWriter> writer;
Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
Scene *scene = CTX_data_scene(C);
/* If not exporting in batch, create single writer for all objects. */
if (!export_params.use_batch) {
writer = std::make_unique<FileWriter>(export_params.filepath, export_params.ascii_format);
}
DEGObjectIterSettings deg_iter_settings{};
deg_iter_settings.depsgraph = depsgraph;
deg_iter_settings.flags = DEG_ITER_OBJECT_FLAG_LINKED_DIRECTLY |
DEG_ITER_OBJECT_FLAG_LINKED_VIA_SET | DEG_ITER_OBJECT_FLAG_VISIBLE |
DEG_ITER_OBJECT_FLAG_DUPLI;
DEG_OBJECT_ITER_BEGIN (&deg_iter_settings, object) {
if (object->type != OB_MESH) {
continue;
}
if (export_params.export_selected_objects && !(object->base_flag & BASE_SELECTED)) {
continue;
}
/* If exporting in batch, create writer for each iteration over objects. */
if (export_params.use_batch) {
/* Get object name by skipping initial "OB" prefix. */
std::string object_name = (object->id.name + 2);
/* Replace spaces with underscores. */
std::replace(object_name.begin(), object_name.end(), ' ', '_');
/* Include object name in the exported file name. */
std::string suffix = object_name + ".stl";
char filepath[FILE_MAX];
BLI_strncpy(filepath, export_params.filepath, FILE_MAX);
BLI_path_extension_replace(filepath, FILE_MAX, suffix.c_str());
writer = std::make_unique<FileWriter>(export_params.filepath, export_params.ascii_format);
}
Object *obj_eval = DEG_get_evaluated_object(depsgraph, object);
Mesh *mesh = export_params.apply_modifiers ? BKE_object_get_evaluated_mesh(obj_eval) :
BKE_object_get_pre_modified_mesh(obj_eval);
/* Calculate transform. */
float global_scale = export_params.global_scale;
if ((scene->unit.system != USER_UNIT_NONE) && export_params.use_scene_unit) {
global_scale *= scene->unit.scale_length;
}
float axes_transform[3][3];
unit_m3(axes_transform);
float xform[4][4];
/* +Y-forward and +Z-up are the default Blender 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(xform, axes_transform, obj_eval->object_to_world);
/* mul_m4_m3m4 does not transform last row of obmat, i.e. location data. */
mul_v3_m3v3(xform[3], axes_transform, obj_eval->object_to_world[3]);
xform[3][3] = obj_eval->object_to_world[3][3];
/* Write triangles. */
const Span<float3> positions = mesh->vert_positions();
const blender::Span<int> corner_verts = mesh->corner_verts();
for (const MLoopTri &loop_tri : mesh->looptris()) {
Triangle t;
for (int i = 0; i < 3; i++) {
float3 pos = positions[corner_verts[loop_tri.tri[i]]];
mul_m4_v3(xform, pos);
pos *= global_scale;
t.vertices[i] = pos;
}
t.normal = math::normal_tri(t.vertices[0], t.vertices[1], t.vertices[2]);
writer->write_triangle(t);
}
}
DEG_OBJECT_ITER_END;
}
} // namespace blender::io::stl

View File

@ -0,0 +1,16 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#pragma once
#include "IO_stl.hh"
namespace blender::io::stl {
/* Main export function used from within Blender. */
void exporter_main(bContext *C, const STLExportParams &export_params);
} // namespace blender::io::stl

View File

@ -0,0 +1,105 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#include <cstdint>
#include <cstdio>
#include <stdexcept>
/* SEP macro from BLI path utils clashes with SEP symbol in fmt headers. */
#undef SEP
#include <fmt/format.h>
#include "stl_export_writer.hh"
#include "BLI_fileops.h"
namespace blender::io::stl {
constexpr size_t BINARY_HEADER_SIZE = 80;
#pragma pack(push, 1)
struct ExportBinaryTriangle {
float3 normal;
float3 vertices[3];
uint16_t attribute_byte_count;
};
#pragma pack(pop)
static_assert(sizeof(ExportBinaryTriangle) == 12 + 12 * 3 + 2,
"ExportBinaryTriangle expected size mismatch");
FileWriter::FileWriter(const char *filepath, bool ascii) : tris_num_(0), ascii_(ascii)
{
file_ = BLI_fopen(filepath, "wb");
if (file_ == nullptr) {
throw std::runtime_error("PLY export: failed to open file");
}
/* Write header */
if (ascii_) {
fmt::print(file_, "solid \n");
}
else {
char header[BINARY_HEADER_SIZE] = {};
fwrite(header, 1, BINARY_HEADER_SIZE, file_);
/* Write placeholder for number of triangles, so that it can be updated later (after all
* triangles have been written). */
fwrite(&tris_num_, sizeof(uint32_t), 1, file_);
}
}
FileWriter::~FileWriter()
{
if (file_ == nullptr) {
return;
}
if (ascii_) {
fmt::print(file_, "endsolid \n");
}
else {
fseek(file_, BINARY_HEADER_SIZE, SEEK_SET);
fwrite(&tris_num_, sizeof(uint32_t), 1, file_);
}
fclose(file_);
}
void FileWriter::write_triangle(const Triangle &t)
{
tris_num_++;
if (ascii_) {
fmt::print(file_,
"facet normal {} {} {}\n"
" outer loop\n"
" vertex {} {} {}\n"
" vertex {} {} {}\n"
" vertex {} {} {}\n"
" endloop\n"
"endfacet\n",
t.normal.x,
t.normal.y,
t.normal.z,
t.vertices[0].x,
t.vertices[0].y,
t.vertices[0].z,
t.vertices[1].x,
t.vertices[1].y,
t.vertices[1].z,
t.vertices[2].x,
t.vertices[2].y,
t.vertices[2].z);
}
else {
ExportBinaryTriangle bin_tri;
bin_tri.normal = t.normal;
bin_tri.vertices[0] = t.vertices[0];
bin_tri.vertices[1] = t.vertices[1];
bin_tri.vertices[2] = t.vertices[2];
bin_tri.attribute_byte_count = 0;
fwrite(&bin_tri, sizeof(ExportBinaryTriangle), 1, file_);
}
}
} // namespace blender::io::stl

View File

@ -0,0 +1,30 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#pragma once
#include "BLI_math_vector_types.hh"
namespace blender::io::stl {
struct Triangle {
float3 normal;
float3 vertices[3];
};
class FileWriter {
public:
FileWriter(const char *filepath, bool ascii);
~FileWriter();
void write_triangle(const Triangle &t);
private:
FILE *file_;
uint32_t tris_num_;
bool ascii_;
};
} // namespace blender::io::stl