diff options
Diffstat (limited to 'source/blender/io')
20 files changed, 1451 insertions, 314 deletions
diff --git a/source/blender/io/collada/CMakeLists.txt b/source/blender/io/collada/CMakeLists.txt index e1645083116..9ce3389257d 100644 --- a/source/blender/io/collada/CMakeLists.txt +++ b/source/blender/io/collada/CMakeLists.txt @@ -135,10 +135,6 @@ if(WITH_BUILDINFO) add_definitions(-DWITH_BUILDINFO) endif() -if(WITH_INTERNATIONAL) - add_definitions(-DWITH_INTERNATIONAL) -endif() - if(CMAKE_COMPILER_IS_GNUCXX) # COLLADAFWArray.h gives error with gcc 4.5 string(APPEND CMAKE_CXX_FLAGS " -fpermissive") diff --git a/source/blender/io/usd/CMakeLists.txt b/source/blender/io/usd/CMakeLists.txt index 12015bf1698..980f33fffa1 100644 --- a/source/blender/io/usd/CMakeLists.txt +++ b/source/blender/io/usd/CMakeLists.txt @@ -64,6 +64,7 @@ set(SRC intern/usd_writer_camera.cc intern/usd_writer_hair.cc intern/usd_writer_light.cc + intern/usd_writer_material.cc intern/usd_writer_mesh.cc intern/usd_writer_metaball.cc intern/usd_writer_transform.cc @@ -89,6 +90,7 @@ set(SRC intern/usd_writer_camera.h intern/usd_writer_hair.h intern/usd_writer_light.h + intern/usd_writer_material.h intern/usd_writer_mesh.h intern/usd_writer_metaball.h intern/usd_writer_transform.h diff --git a/source/blender/io/usd/intern/usd_reader_camera.cc b/source/blender/io/usd/intern/usd_reader_camera.cc index 2732ed5770d..1d001e19140 100644 --- a/source/blender/io/usd/intern/usd_reader_camera.cc +++ b/source/blender/io/usd/intern/usd_reader_camera.cc @@ -72,7 +72,7 @@ void USDCameraReader::read_object_data(Main *bmain, const double motionSampleTim cam_prim.GetHorizontalApertureAttr().Get(&horAp, motionSampleTime); bcam->lens = val.Get<float>(); - /* TODO(makowalski) */ + /* TODO(@makowalski): support sensor size. */ #if 0 bcam->sensor_x = 0.0f; bcam->sensor_y = 0.0f; diff --git a/source/blender/io/usd/intern/usd_writer_abstract.cc b/source/blender/io/usd/intern/usd_writer_abstract.cc index 2b5326eb4c1..a358c563c88 100644 --- a/source/blender/io/usd/intern/usd_writer_abstract.cc +++ b/source/blender/io/usd/intern/usd_writer_abstract.cc @@ -18,11 +18,15 @@ */ #include "usd_writer_abstract.h" #include "usd_hierarchy_iterator.h" +#include "usd_writer_material.h" #include <pxr/base/tf/stringUtils.h> +#include "BKE_customdata.h" #include "BLI_assert.h" +#include "DNA_mesh_types.h" + /* TfToken objects are not cheap to construct, so we do it once. */ namespace usdtokens { /* Materials */ @@ -34,6 +38,19 @@ static const pxr::TfToken roughness("roughness", pxr::TfToken::Immortal); static const pxr::TfToken surface("surface", pxr::TfToken::Immortal); } // namespace usdtokens +static std::string get_mesh_active_uvlayer_name(const Object *ob) +{ + if (!ob || ob->type != OB_MESH || !ob->data) { + return ""; + } + + const Mesh *me = static_cast<Mesh *>(ob->data); + + const char *name = CustomData_get_active_layer_name(&me->ldata, CD_MLOOPUV); + + return name ? name : ""; +} + namespace blender::io::usd { USDAbstractWriter::USDAbstractWriter(const USDExporterContext &usd_export_context) @@ -78,7 +95,8 @@ const pxr::SdfPath &USDAbstractWriter::usd_path() const return usd_export_context_.usd_path; } -pxr::UsdShadeMaterial USDAbstractWriter::ensure_usd_material(Material *material) +pxr::UsdShadeMaterial USDAbstractWriter::ensure_usd_material(const HierarchyContext &context, + Material *material) { static pxr::SdfPath material_library_path("/_materials"); pxr::UsdStageRefPtr stage = usd_export_context_.stage; @@ -92,17 +110,14 @@ pxr::UsdShadeMaterial USDAbstractWriter::ensure_usd_material(Material *material) } usd_material = pxr::UsdShadeMaterial::Define(stage, usd_path); - /* Construct the shader. */ - pxr::SdfPath shader_path = usd_path.AppendChild(usdtokens::preview_shader); - pxr::UsdShadeShader shader = pxr::UsdShadeShader::Define(stage, shader_path); - shader.CreateIdAttr(pxr::VtValue(usdtokens::preview_surface)); - shader.CreateInput(usdtokens::diffuse_color, pxr::SdfValueTypeNames->Color3f) - .Set(pxr::GfVec3f(material->r, material->g, material->b)); - shader.CreateInput(usdtokens::roughness, pxr::SdfValueTypeNames->Float).Set(material->roughness); - shader.CreateInput(usdtokens::metallic, pxr::SdfValueTypeNames->Float).Set(material->metallic); - - /* Connect the shader and the material together. */ - usd_material.CreateSurfaceOutput().ConnectToSource(shader, usdtokens::surface); + if (material->use_nodes && this->usd_export_context_.export_params.generate_preview_surface) { + std::string active_uv = get_mesh_active_uvlayer_name(context.object); + create_usd_preview_surface_material( + this->usd_export_context_, material, usd_material, active_uv); + } + else { + create_usd_viewport_material(this->usd_export_context_, material, usd_material); + } return usd_material; } diff --git a/source/blender/io/usd/intern/usd_writer_abstract.h b/source/blender/io/usd/intern/usd_writer_abstract.h index dd81dd47c83..c67aa824263 100644 --- a/source/blender/io/usd/intern/usd_writer_abstract.h +++ b/source/blender/io/usd/intern/usd_writer_abstract.h @@ -69,7 +69,7 @@ class USDAbstractWriter : public AbstractHierarchyWriter { virtual void do_write(HierarchyContext &context) = 0; pxr::UsdTimeCode get_export_time_code() const; - pxr::UsdShadeMaterial ensure_usd_material(Material *material); + pxr::UsdShadeMaterial ensure_usd_material(const HierarchyContext &context, Material *material); void write_visibility(const HierarchyContext &context, const pxr::UsdTimeCode timecode, diff --git a/source/blender/io/usd/intern/usd_writer_material.cc b/source/blender/io/usd/intern/usd_writer_material.cc new file mode 100644 index 00000000000..5ce04339503 --- /dev/null +++ b/source/blender/io/usd/intern/usd_writer_material.cc @@ -0,0 +1,767 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "usd_writer_material.h" + +#include "usd.h" +#include "usd_exporter_context.h" + +#include "BKE_image.h" +#include "BKE_main.h" +#include "BKE_node.h" + +#include "BLI_fileops.h" +#include "BLI_linklist.h" +#include "BLI_listbase.h" +#include "BLI_math.h" +#include "BLI_path_util.h" +#include "BLI_string.h" + +#include "DNA_material_types.h" + +#include "MEM_guardedalloc.h" + +#include "WM_api.h" + +#include <pxr/base/tf/stringUtils.h> +#include <pxr/pxr.h> +#include <pxr/usd/usdGeom/scope.h> + +#include <iostream> + +/* TfToken objects are not cheap to construct, so we do it once. */ +namespace usdtokens { +// Materials +static const pxr::TfToken clearcoat("clearcoat", pxr::TfToken::Immortal); +static const pxr::TfToken clearcoatRoughness("clearcoatRoughness", pxr::TfToken::Immortal); +static const pxr::TfToken diffuse_color("diffuseColor", pxr::TfToken::Immortal); +static const pxr::TfToken metallic("metallic", pxr::TfToken::Immortal); +static const pxr::TfToken preview_shader("previewShader", pxr::TfToken::Immortal); +static const pxr::TfToken preview_surface("UsdPreviewSurface", pxr::TfToken::Immortal); +static const pxr::TfToken uv_texture("UsdUVTexture", pxr::TfToken::Immortal); +static const pxr::TfToken primvar_float2("UsdPrimvarReader_float2", pxr::TfToken::Immortal); +static const pxr::TfToken roughness("roughness", pxr::TfToken::Immortal); +static const pxr::TfToken specular("specular", pxr::TfToken::Immortal); +static const pxr::TfToken opacity("opacity", pxr::TfToken::Immortal); +static const pxr::TfToken surface("surface", pxr::TfToken::Immortal); +static const pxr::TfToken perspective("perspective", pxr::TfToken::Immortal); +static const pxr::TfToken orthographic("orthographic", pxr::TfToken::Immortal); +static const pxr::TfToken rgb("rgb", pxr::TfToken::Immortal); +static const pxr::TfToken r("r", pxr::TfToken::Immortal); +static const pxr::TfToken g("g", pxr::TfToken::Immortal); +static const pxr::TfToken b("b", pxr::TfToken::Immortal); +static const pxr::TfToken st("st", pxr::TfToken::Immortal); +static const pxr::TfToken result("result", pxr::TfToken::Immortal); +static const pxr::TfToken varname("varname", pxr::TfToken::Immortal); +static const pxr::TfToken out("out", pxr::TfToken::Immortal); +static const pxr::TfToken normal("normal", pxr::TfToken::Immortal); +static const pxr::TfToken ior("ior", pxr::TfToken::Immortal); +static const pxr::TfToken file("file", pxr::TfToken::Immortal); +static const pxr::TfToken preview("preview", pxr::TfToken::Immortal); +static const pxr::TfToken raw("raw", pxr::TfToken::Immortal); +static const pxr::TfToken sRGB("sRGB", pxr::TfToken::Immortal); +static const pxr::TfToken sourceColorSpace("sourceColorSpace", pxr::TfToken::Immortal); +static const pxr::TfToken Shader("Shader", pxr::TfToken::Immortal); +} // namespace usdtokens + +/* Cycles specific tokens. */ +namespace cyclestokens { +static const pxr::TfToken UVMap("UVMap", pxr::TfToken::Immortal); +} // namespace cyclestokens + +namespace blender::io::usd { + +/* Preview surface input specification. */ +struct InputSpec { + pxr::TfToken input_name; + pxr::SdfValueTypeName input_type; + pxr::TfToken source_name; + /* Whether a default value should be set + * if the node socket has not input. Usually + * false for the Normal input. */ + bool set_default_value; +}; + +/* Map Blender socket names to USD Preview Surface InputSpec structs. */ +typedef std::map<std::string, InputSpec> InputSpecMap; + +/* Static function forward declarations. */ +static pxr::UsdShadeShader create_usd_preview_shader(const USDExporterContext &usd_export_context, + pxr::UsdShadeMaterial &material, + const char *name, + int type); +static pxr::UsdShadeShader create_usd_preview_shader(const USDExporterContext &usd_export_context, + pxr::UsdShadeMaterial &material, + bNode *node); +static void create_uvmap_shader(const USDExporterContext &usd_export_context, + bNode *tex_node, + pxr::UsdShadeMaterial &usd_material, + pxr::UsdShadeShader &usd_tex_shader, + const pxr::TfToken &default_uv); +static void export_texture(bNode *node, + const pxr::UsdStageRefPtr stage, + const bool allow_overwrite = false); +static bNode *find_bsdf_node(Material *material); +static void get_absolute_path(Image *ima, char *r_path); +static std::string get_tex_image_asset_path(bNode *node, + const pxr::UsdStageRefPtr stage, + const USDExportParams &export_params); +static InputSpecMap &preview_surface_input_map(); +static bNode *traverse_channel(bNodeSocket *input, short target_type); + +template<typename T1, typename T2> +void create_input(pxr::UsdShadeShader &shader, const InputSpec &spec, const void *value); + +void create_usd_preview_surface_material(const USDExporterContext &usd_export_context, + Material *material, + pxr::UsdShadeMaterial &usd_material, + const std::string &default_uv) +{ + if (!material) { + return; + } + + /* Define a 'preview' scope beneath the material which will contain the preview shaders. */ + pxr::UsdGeomScope::Define(usd_export_context.stage, + usd_material.GetPath().AppendChild(usdtokens::preview)); + + /* Default map when creating UV primvar reader shaders. */ + pxr::TfToken default_uv_sampler = default_uv.empty() ? cyclestokens::UVMap : + pxr::TfToken(default_uv); + + /* We only handle the first instance of either principled or + * diffuse bsdf nodes in the material's node tree, because + * USD Preview Surface has no concept of layering materials. */ + bNode *node = find_bsdf_node(material); + if (!node) { + return; + } + + pxr::UsdShadeShader preview_surface = create_usd_preview_shader( + usd_export_context, usd_material, node); + + const InputSpecMap &input_map = preview_surface_input_map(); + + /* Set the preview surface inputs. */ + LISTBASE_FOREACH (bNodeSocket *, sock, &node->inputs) { + + /* Check if this socket is mapped to a USD preview shader input. */ + const InputSpecMap::const_iterator it = input_map.find(sock->name); + + if (it == input_map.end()) { + continue; + } + + pxr::UsdShadeShader created_shader; + + bNode *input_node = traverse_channel(sock, SH_NODE_TEX_IMAGE); + + const InputSpec &input_spec = it->second; + + if (input_node) { + /* Create connection. */ + created_shader = create_usd_preview_shader(usd_export_context, usd_material, input_node); + + preview_surface.CreateInput(input_spec.input_name, input_spec.input_type) + .ConnectToSource(created_shader, input_spec.source_name); + } + else if (input_spec.set_default_value) { + /* Set hardcoded value. */ + switch (sock->type) { + case SOCK_FLOAT: { + create_input<bNodeSocketValueFloat, float>( + preview_surface, input_spec, sock->default_value); + } break; + case SOCK_VECTOR: { + create_input<bNodeSocketValueVector, pxr::GfVec3f>( + preview_surface, input_spec, sock->default_value); + } break; + case SOCK_RGBA: { + create_input<bNodeSocketValueRGBA, pxr::GfVec3f>( + preview_surface, input_spec, sock->default_value); + } break; + default: + break; + } + } + + /* If any input texture node has been found, export the texture, if necessary, + * and look for a connected uv node. */ + if (!(created_shader && input_node && input_node->type == SH_NODE_TEX_IMAGE)) { + continue; + } + + if (usd_export_context.export_params.export_textures) { + export_texture(input_node, + usd_export_context.stage, + usd_export_context.export_params.overwrite_textures); + } + + create_uvmap_shader( + usd_export_context, input_node, usd_material, created_shader, default_uv_sampler); + } +} + +void create_usd_viewport_material(const USDExporterContext &usd_export_context, + Material *material, + pxr::UsdShadeMaterial &usd_material) +{ + /* Construct the shader. */ + pxr::SdfPath shader_path = usd_material.GetPath().AppendChild(usdtokens::preview_shader); + pxr::UsdShadeShader shader = pxr::UsdShadeShader::Define(usd_export_context.stage, shader_path); + + shader.CreateIdAttr(pxr::VtValue(usdtokens::preview_surface)); + shader.CreateInput(usdtokens::diffuse_color, pxr::SdfValueTypeNames->Color3f) + .Set(pxr::GfVec3f(material->r, material->g, material->b)); + shader.CreateInput(usdtokens::roughness, pxr::SdfValueTypeNames->Float).Set(material->roughness); + shader.CreateInput(usdtokens::metallic, pxr::SdfValueTypeNames->Float).Set(material->metallic); + + /* Connect the shader and the material together. */ + usd_material.CreateSurfaceOutput().ConnectToSource(shader, usdtokens::surface); +} + +/* Return USD Preview Surface input map singleton. */ +static InputSpecMap &preview_surface_input_map() +{ + static InputSpecMap input_map = { + {"Base Color", + {usdtokens::diffuse_color, pxr::SdfValueTypeNames->Float3, usdtokens::rgb, true}}, + {"Color", {usdtokens::diffuse_color, pxr::SdfValueTypeNames->Float3, usdtokens::rgb, true}}, + {"Roughness", {usdtokens::roughness, pxr::SdfValueTypeNames->Float, usdtokens::r, true}}, + {"Metallic", {usdtokens::metallic, pxr::SdfValueTypeNames->Float, usdtokens::r, true}}, + {"Specular", {usdtokens::specular, pxr::SdfValueTypeNames->Float, usdtokens::r, true}}, + {"Alpha", {usdtokens::opacity, pxr::SdfValueTypeNames->Float, usdtokens::r, true}}, + {"IOR", {usdtokens::ior, pxr::SdfValueTypeNames->Float, usdtokens::r, true}}, + /* Note that for the Normal input set_default_value is false. */ + {"Normal", {usdtokens::normal, pxr::SdfValueTypeNames->Float3, usdtokens::rgb, false}}, + {"Clearcoat", {usdtokens::clearcoat, pxr::SdfValueTypeNames->Float, usdtokens::r, true}}, + {"Clearcoat Roughness", + {usdtokens::clearcoatRoughness, pxr::SdfValueTypeNames->Float, usdtokens::r, true}}, + }; + + return input_map; +} + +/* Create an input on the given shader with name and type + * provided by the InputSpec and assign the given value to the + * input. Parameters T1 and T2 indicate the Blender and USD + * value types, respectively. */ +template<typename T1, typename T2> +void create_input(pxr::UsdShadeShader &shader, const InputSpec &spec, const void *value) +{ + const T1 *cast_value = static_cast<const T1 *>(value); + shader.CreateInput(spec.input_name, spec.input_type).Set(T2(cast_value->value)); +} + +/* Find the UVMAP node input to the given texture image node and convert it + * to a USD primvar reader shader. If no UVMAP node is found, create a primvar + * reader for the given default uv set. The primvar reader will be attached to + * the 'st' input of the given USD texture shader. */ +static void create_uvmap_shader(const USDExporterContext &usd_export_context, + bNode *tex_node, + pxr::UsdShadeMaterial &usd_material, + pxr::UsdShadeShader &usd_tex_shader, + const pxr::TfToken &default_uv) +{ + bool found_uv_node = false; + + /* Find UV input to the texture node. */ + LISTBASE_FOREACH (bNodeSocket *, tex_node_sock, &tex_node->inputs) { + + if (!tex_node_sock->link || !STREQ(tex_node_sock->name, "Vector")) { + continue; + } + + bNode *uv_node = traverse_channel(tex_node_sock, SH_NODE_UVMAP); + if (uv_node == nullptr) { + continue; + } + + pxr::UsdShadeShader uv_shader = create_usd_preview_shader( + usd_export_context, usd_material, uv_node); + + if (!uv_shader.GetPrim().IsValid()) { + continue; + } + + found_uv_node = true; + + if (NodeShaderUVMap *shader_uv_map = static_cast<NodeShaderUVMap *>(uv_node->storage)) { + /* We need to make valid here because actual uv primvar has been. */ + std::string uv_set = pxr::TfMakeValidIdentifier(shader_uv_map->uv_map); + + uv_shader.CreateInput(usdtokens::varname, pxr::SdfValueTypeNames->Token) + .Set(pxr::TfToken(uv_set)); + usd_tex_shader.CreateInput(usdtokens::st, pxr::SdfValueTypeNames->Float2) + .ConnectToSource(uv_shader, usdtokens::result); + } + else { + uv_shader.CreateInput(usdtokens::varname, pxr::SdfValueTypeNames->Token).Set(default_uv); + usd_tex_shader.CreateInput(usdtokens::st, pxr::SdfValueTypeNames->Float2) + .ConnectToSource(uv_shader, usdtokens::result); + } + } + + if (!found_uv_node) { + /* No UVMAP node was linked to the texture node. However, we generate + * a primvar reader node that specifies the UV set to sample, as some + * DCCs require this. */ + + pxr::UsdShadeShader uv_shader = create_usd_preview_shader( + usd_export_context, usd_material, "uvmap", SH_NODE_TEX_COORD); + + if (uv_shader.GetPrim().IsValid()) { + uv_shader.CreateInput(usdtokens::varname, pxr::SdfValueTypeNames->Token).Set(default_uv); + usd_tex_shader.CreateInput(usdtokens::st, pxr::SdfValueTypeNames->Float2) + .ConnectToSource(uv_shader, usdtokens::result); + } + } +} + +/* Generate a file name for an in-memory image that doesn't have a + * filepath already defined. */ +static std::string get_in_memory_texture_filename(Image *ima) +{ + bool is_dirty = BKE_image_is_dirty(ima); + bool is_generated = ima->source == IMA_SRC_GENERATED; + bool is_packed = BKE_image_has_packedfile(ima); + if (!(is_generated || is_dirty || is_packed)) { + return ""; + } + + /* Determine the correct file extension from the image format. */ + ImBuf *imbuf = BKE_image_acquire_ibuf(ima, nullptr, nullptr); + if (!imbuf) { + return ""; + } + + ImageFormatData imageFormat; + BKE_imbuf_to_image_format(&imageFormat, imbuf); + + char file_name[FILE_MAX]; + /* Use the image name for the file name. */ + strcpy(file_name, ima->id.name + 2); + + BKE_image_path_ensure_ext_from_imformat(file_name, &imageFormat); + + return file_name; +} + +static void export_in_memory_texture(Image *ima, + const std::string &export_dir, + const bool allow_overwrite) +{ + char image_abs_path[FILE_MAX]; + + char file_name[FILE_MAX]; + if (strlen(ima->filepath) > 0) { + get_absolute_path(ima, image_abs_path); + BLI_split_file_part(image_abs_path, file_name, FILE_MAX); + } + else { + /* Use the image name for the file name. */ + strcpy(file_name, ima->id.name + 2); + } + + ImBuf *imbuf = BKE_image_acquire_ibuf(ima, nullptr, nullptr); + if (!imbuf) { + return; + } + + ImageFormatData imageFormat; + BKE_imbuf_to_image_format(&imageFormat, imbuf); + + /* This image in its current state only exists in Blender memory. + * So we have to export it. The export will keep the image state intact, + * so the exported file will not be associated with the image. */ + + BKE_image_path_ensure_ext_from_imformat(file_name, &imageFormat); + + char export_path[FILE_MAX]; + BLI_path_join(export_path, FILE_MAX, export_dir.c_str(), file_name, nullptr); + + if (!allow_overwrite && BLI_exists(export_path)) { + return; + } + + if ((BLI_path_cmp_normalized(export_path, image_abs_path) == 0) && BLI_exists(image_abs_path)) { + /* As a precaution, don't overwrite the original path. */ + return; + } + + std::cout << "Exporting in-memory texture to " << export_path << std::endl; + + if (BKE_imbuf_write_as(imbuf, export_path, &imageFormat, true) == 0) { + WM_reportf(RPT_WARNING, "USD export: couldn't export in-memory texture to %s", export_path); + } +} + +/* Get the absolute filepath of the given image. Assumes + * r_path result array is of length FILE_MAX. */ +static void get_absolute_path(Image *ima, char *r_path) +{ + /* Make absolute source path. */ + BLI_strncpy(r_path, ima->filepath, FILE_MAX); + BLI_path_abs(r_path, ID_BLEND_PATH_FROM_GLOBAL(&ima->id)); + BLI_path_normalize(nullptr, r_path); +} + +static pxr::TfToken get_node_tex_image_color_space(bNode *node) +{ + if (!node->id) { + return pxr::TfToken(); + } + + Image *ima = reinterpret_cast<Image *>(node->id); + + if (strcmp(ima->colorspace_settings.name, "Raw") == 0) { + return usdtokens::raw; + } + if (strcmp(ima->colorspace_settings.name, "Non-Color") == 0) { + return usdtokens::raw; + } + if (strcmp(ima->colorspace_settings.name, "sRGB") == 0) { + return usdtokens::sRGB; + } + + return pxr::TfToken(); +} + +/* Search the upstream nodes connected to the given socket and return the first occurrence + * of the node of the given type. Return null if no node of this type was found. */ +static bNode *traverse_channel(bNodeSocket *input, const short target_type) +{ + if (!input->link) { + return nullptr; + } + + bNode *linked_node = input->link->fromnode; + if (linked_node->type == target_type) { + /* Return match. */ + return linked_node; + } + + /* Recursively traverse the linked node's sockets. */ + LISTBASE_FOREACH (bNodeSocket *, sock, &linked_node->inputs) { + if (bNode *found_node = traverse_channel(sock, target_type)) { + return found_node; + } + } + + return nullptr; +} + +/* Returns the first occurrence of a principled BSDF or a diffuse BSDF node found in the given + * material's node tree. Returns null if no instance of either type was found.*/ +static bNode *find_bsdf_node(Material *material) +{ + LISTBASE_FOREACH (bNode *, node, &material->nodetree->nodes) { + if (node->type == SH_NODE_BSDF_PRINCIPLED || node->type == SH_NODE_BSDF_DIFFUSE) { + return node; + } + } + + return nullptr; +} + +/* Creates a USD Preview Surface shader based on the given cycles node name and type. */ +static pxr::UsdShadeShader create_usd_preview_shader(const USDExporterContext &usd_export_context, + pxr::UsdShadeMaterial &material, + const char *name, + const int type) +{ + pxr::SdfPath shader_path = material.GetPath() + .AppendChild(usdtokens::preview) + .AppendChild(pxr::TfToken(pxr::TfMakeValidIdentifier(name))); + pxr::UsdShadeShader shader = pxr::UsdShadeShader::Define(usd_export_context.stage, shader_path); + + switch (type) { + case SH_NODE_TEX_IMAGE: { + shader.CreateIdAttr(pxr::VtValue(usdtokens::uv_texture)); + break; + } + case SH_NODE_TEX_COORD: + case SH_NODE_UVMAP: { + shader.CreateIdAttr(pxr::VtValue(usdtokens::primvar_float2)); + break; + } + case SH_NODE_BSDF_DIFFUSE: + case SH_NODE_BSDF_PRINCIPLED: { + shader.CreateIdAttr(pxr::VtValue(usdtokens::preview_surface)); + material.CreateSurfaceOutput().ConnectToSource(shader, usdtokens::surface); + break; + } + + default: + break; + } + + return shader; +} + +/* Creates a USD Preview Surface shader based on the given cycles shading node. */ +static pxr::UsdShadeShader create_usd_preview_shader(const USDExporterContext &usd_export_context, + pxr::UsdShadeMaterial &material, + bNode *node) +{ + pxr::UsdShadeShader shader = create_usd_preview_shader( + usd_export_context, material, node->name, node->type); + + if (node->type != SH_NODE_TEX_IMAGE) { + return shader; + } + + /* For texture image nodes we set the image path and color space. */ + std::string imagePath = get_tex_image_asset_path( + node, usd_export_context.stage, usd_export_context.export_params); + if (!imagePath.empty()) { + shader.CreateInput(usdtokens::file, pxr::SdfValueTypeNames->Asset) + .Set(pxr::SdfAssetPath(imagePath)); + } + + pxr::TfToken colorSpace = get_node_tex_image_color_space(node); + if (!colorSpace.IsEmpty()) { + shader.CreateInput(usdtokens::sourceColorSpace, pxr::SdfValueTypeNames->Token).Set(colorSpace); + } + + return shader; +} + +static std::string get_tex_image_asset_path(Image *ima) +{ + char filepath[FILE_MAX]; + get_absolute_path(ima, filepath); + + return std::string(filepath); +} + +/* Gets an asset path for the given texture image node. The resulting path + * may be absolute, relative to the USD file, or in a 'textures' directory + * in the same directory as the USD file, depending on the export parameters. + * The filename is typically the image filepath but might also be automatically + * generated based on the image name for in-memory textures when exporting textures. + * This function may return an empty string if the image does not have a filepath + * assigned and no asset path could be determined. */ +static std::string get_tex_image_asset_path(bNode *node, + const pxr::UsdStageRefPtr stage, + const USDExportParams &export_params) +{ + Image *ima = reinterpret_cast<Image *>(node->id); + if (!ima) { + return ""; + } + + std::string path; + + if (strlen(ima->filepath) > 0) { + /* Get absolute path. */ + path = get_tex_image_asset_path(ima); + } + else if (export_params.export_textures) { + /* Image has no filepath, but since we are exporting textures, + * check if this is an in-memory texture for which we can + * generate a file name. */ + path = get_in_memory_texture_filename(ima); + } + + if (path.empty()) { + return path; + } + + if (export_params.export_textures) { + /* The texture is exported to a 'textures' directory next to the + * USD root layer. */ + + char exp_path[FILE_MAX]; + char file_path[FILE_MAX]; + BLI_split_file_part(path.c_str(), file_path, FILE_MAX); + + if (export_params.relative_texture_paths) { + BLI_path_join(exp_path, FILE_MAX, ".", "textures", file_path, nullptr); + } + else { + /* Create absolute path in the textures directory. */ + pxr::SdfLayerHandle layer = stage->GetRootLayer(); + std::string stage_path = layer->GetRealPath(); + if (stage_path.empty()) { + return path; + } + + char dir_path[FILE_MAX]; + BLI_split_dir_part(stage_path.c_str(), dir_path, FILE_MAX); + BLI_path_join(exp_path, FILE_MAX, dir_path, "textures", file_path, nullptr); + } + return exp_path; + } + + if (export_params.relative_texture_paths) { + /* Get the path relative to the USD. */ + pxr::SdfLayerHandle layer = stage->GetRootLayer(); + std::string stage_path = layer->GetRealPath(); + if (stage_path.empty()) { + return path; + } + + char rel_path[FILE_MAX]; + strcpy(rel_path, path.c_str()); + + BLI_path_rel(rel_path, stage_path.c_str()); + + /* BLI_path_rel adds '//' as a prefix to the path, if + * generating the relative path was successful. */ + if (rel_path[0] != '/' || rel_path[1] != '/') { + /* No relative path generated. */ + return path; + } + + return rel_path + 2; + } + + return path; +} + +/* If the given image is tiled, copy the image tiles to the given + * destination directory. */ +static void copy_tiled_textures(Image *ima, + const std::string &dest_dir, + const bool allow_overwrite) +{ + char src_path[FILE_MAX]; + get_absolute_path(ima, src_path); + + eUDIM_TILE_FORMAT tile_format; + char *udim_pattern = BKE_image_get_tile_strformat(src_path, &tile_format); + + /* Only <UDIM> tile formats are supported by USD right now. */ + if (tile_format != UDIM_TILE_FORMAT_UDIM) { + std::cout << "WARNING: unsupported tile format for `" << src_path << "`" << std::endl; + MEM_SAFE_FREE(udim_pattern); + return; + } + + /* Copy all tiles. */ + LISTBASE_FOREACH (ImageTile *, tile, &ima->tiles) { + char src_tile_path[FILE_MAX]; + BKE_image_set_filepath_from_tile_number( + src_tile_path, udim_pattern, tile_format, tile->tile_number); + + char dest_filename[FILE_MAXFILE]; + BLI_split_file_part(src_tile_path, dest_filename, sizeof(dest_filename)); + + char dest_tile_path[FILE_MAX]; + BLI_path_join(dest_tile_path, FILE_MAX, dest_dir.c_str(), dest_filename, nullptr); + + if (!allow_overwrite && BLI_exists(dest_tile_path)) { + continue; + } + + if (BLI_path_cmp_normalized(src_tile_path, dest_tile_path) == 0) { + /* Source and destination paths are the same, don't copy. */ + continue; + } + + std::cout << "Copying texture tile from " << src_tile_path << " to " << dest_tile_path + << std::endl; + + /* Copy the file. */ + if (BLI_copy(src_tile_path, dest_tile_path) != 0) { + WM_reportf(RPT_WARNING, + "USD export: couldn't copy texture tile from %s to %s", + src_tile_path, + dest_tile_path); + } + } + MEM_SAFE_FREE(udim_pattern); +} + +/* Copy the given image to the destination directory. */ +static void copy_single_file(Image *ima, const std::string &dest_dir, const bool allow_overwrite) +{ + char source_path[FILE_MAX]; + get_absolute_path(ima, source_path); + + char file_name[FILE_MAX]; + BLI_split_file_part(source_path, file_name, FILE_MAX); + + char dest_path[FILE_MAX]; + BLI_path_join(dest_path, FILE_MAX, dest_dir.c_str(), file_name, nullptr); + + if (!allow_overwrite && BLI_exists(dest_path)) { + return; + } + + if (BLI_path_cmp_normalized(source_path, dest_path) == 0) { + /* Source and destination paths are the same, don't copy. */ + return; + } + + std::cout << "Copying texture from " << source_path << " to " << dest_path << std::endl; + + /* Copy the file. */ + if (BLI_copy(source_path, dest_path) != 0) { + WM_reportf( + RPT_WARNING, "USD export: couldn't copy texture from %s to %s", source_path, dest_path); + } +} + +/* Export the given texture node's image to a 'textures' directory + * next to given stage's root layer USD. + * Based on ImagesExporter::export_UV_Image() */ +static void export_texture(bNode *node, + const pxr::UsdStageRefPtr stage, + const bool allow_overwrite) +{ + if (node->type != SH_NODE_TEX_IMAGE && node->type != SH_NODE_TEX_ENVIRONMENT) { + return; + } + + Image *ima = reinterpret_cast<Image *>(node->id); + if (!ima) { + return; + } + + pxr::SdfLayerHandle layer = stage->GetRootLayer(); + std::string stage_path = layer->GetRealPath(); + if (stage_path.empty()) { + return; + } + + char usd_dir_path[FILE_MAX]; + BLI_split_dir_part(stage_path.c_str(), usd_dir_path, FILE_MAX); + + char tex_dir_path[FILE_MAX]; + BLI_path_join(tex_dir_path, FILE_MAX, usd_dir_path, "textures", SEP_STR, nullptr); + + BLI_dir_create_recursive(tex_dir_path); + + const bool is_dirty = BKE_image_is_dirty(ima); + const bool is_generated = ima->source == IMA_SRC_GENERATED; + const bool is_packed = BKE_image_has_packedfile(ima); + + std::string dest_dir(tex_dir_path); + + if (is_generated || is_dirty || is_packed) { + export_in_memory_texture(ima, dest_dir, allow_overwrite); + } + else if (ima->source == IMA_SRC_TILED) { + copy_tiled_textures(ima, dest_dir, allow_overwrite); + } + else { + copy_single_file(ima, dest_dir, allow_overwrite); + } +} + +} // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_writer_material.h b/source/blender/io/usd/intern/usd_writer_material.h new file mode 100644 index 00000000000..435ac41c4bf --- /dev/null +++ b/source/blender/io/usd/intern/usd_writer_material.h @@ -0,0 +1,57 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include <pxr/pxr.h> +#include <pxr/usd/usd/prim.h> +#include <pxr/usd/usd/stage.h> +#include <pxr/usd/usdShade/material.h> + +#include <string> + +struct bNode; +struct bNodeTree; +struct Material; +struct USDExportParams; + +namespace blender::io::usd { + +struct USDExporterContext; + +/** + * Entry point to create an approximate USD Preview Surface network from a Cycles node graph. + * Due to the limited nodes in the USD Preview Surface specification, only the following nodes + * are supported: + * - UVMap + * - Texture Coordinate + * - Image Texture + * - Principled BSDF + * More may be added in the future. + * + * \param default_uv: used as the default UV set name sampled by the `primvar` + * reader shaders generated for image texture nodes that don't have an attached UVMap node. + */ +void create_usd_preview_surface_material(const USDExporterContext &usd_export_context, + Material *material, + pxr::UsdShadeMaterial &usd_material, + const std::string &default_uv = ""); + +/* Entry point to create USD Shade Material network from Blender viewport display settings. */ +void create_usd_viewport_material(const USDExporterContext &usd_export_context, + Material *material, + pxr::UsdShadeMaterial &usd_material); + +} // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_writer_mesh.cc b/source/blender/io/usd/intern/usd_writer_mesh.cc index b061a2ff795..1d3a5f5280d 100644 --- a/source/blender/io/usd/intern/usd_writer_mesh.cc +++ b/source/blender/io/usd/intern/usd_writer_mesh.cc @@ -361,7 +361,7 @@ void USDGenericMeshWriter::assign_materials(const HierarchyContext &context, continue; } - pxr::UsdShadeMaterial usd_material = ensure_usd_material(material); + pxr::UsdShadeMaterial usd_material = ensure_usd_material(context, material); material_binding_api.Bind(usd_material); /* USD seems to support neither per-material nor per-face-group double-sidedness, so we just @@ -395,7 +395,7 @@ void USDGenericMeshWriter::assign_materials(const HierarchyContext &context, continue; } - pxr::UsdShadeMaterial usd_material = ensure_usd_material(material); + pxr::UsdShadeMaterial usd_material = ensure_usd_material(context, material); pxr::TfToken material_name = usd_material.GetPath().GetNameToken(); pxr::UsdGeomSubset usd_face_subset = material_binding_api.CreateMaterialBindSubset( diff --git a/source/blender/io/usd/usd.h b/source/blender/io/usd/usd.h index 16bd826ecdd..505f917ede5 100644 --- a/source/blender/io/usd/usd.h +++ b/source/blender/io/usd/usd.h @@ -41,6 +41,10 @@ struct USDExportParams { bool visible_objects_only; bool use_instancing; enum eEvaluationMode evaluation_mode; + bool generate_preview_surface; + bool export_textures; + bool overwrite_textures; + bool relative_texture_paths; }; struct USDImportParams { diff --git a/source/blender/io/wavefront_obj/CMakeLists.txt b/source/blender/io/wavefront_obj/CMakeLists.txt index 296dd70b5a2..0b1be7946cf 100644 --- a/source/blender/io/wavefront_obj/CMakeLists.txt +++ b/source/blender/io/wavefront_obj/CMakeLists.txt @@ -56,6 +56,12 @@ set(LIB bf_blenkernel ) +if(WITH_TBB) + add_definitions(-DWITH_TBB) + list(APPEND INC_SYS ${TBB_INCLUDE_DIRS}) + list(APPEND LIB ${TBB_LIBRARIES}) +endif() + blender_add_lib(bf_wavefront_obj "${SRC}" "${INC}" "${INC_SYS}" "${LIB}") if(WITH_GTESTS) diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc index d31353c4a76..87f87e37a7e 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc +++ b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc @@ -24,6 +24,7 @@ #include "BKE_blender_version.h" #include "BLI_path_util.h" +#include "BLI_task.hh" #include "obj_export_mesh.hh" #include "obj_export_mtl.hh" @@ -52,68 +53,75 @@ const char *DEFORM_GROUP_DISABLED = "off"; * So an empty material name is written. */ const char *MATERIAL_GROUP_DISABLED = ""; -void OBJWriter::write_vert_uv_normal_indices(Span<int> vert_indices, +void OBJWriter::write_vert_uv_normal_indices(FormatHandler<eFileType::OBJ> &fh, + const IndexOffsets &offsets, + Span<int> vert_indices, Span<int> uv_indices, Span<int> normal_indices) const { BLI_assert(vert_indices.size() == uv_indices.size() && vert_indices.size() == normal_indices.size()); - file_handler_->write<eOBJSyntaxElement::poly_element_begin>(); + fh.write<eOBJSyntaxElement::poly_element_begin>(); for (int j = 0; j < vert_indices.size(); j++) { - file_handler_->write<eOBJSyntaxElement::vertex_uv_normal_indices>( - vert_indices[j] + index_offsets_.vertex_offset + 1, - uv_indices[j] + index_offsets_.uv_vertex_offset + 1, - normal_indices[j] + index_offsets_.normal_offset + 1); + fh.write<eOBJSyntaxElement::vertex_uv_normal_indices>( + vert_indices[j] + offsets.vertex_offset + 1, + uv_indices[j] + offsets.uv_vertex_offset + 1, + normal_indices[j] + offsets.normal_offset + 1); } - file_handler_->write<eOBJSyntaxElement::poly_element_end>(); + fh.write<eOBJSyntaxElement::poly_element_end>(); } -void OBJWriter::write_vert_normal_indices(Span<int> vert_indices, +void OBJWriter::write_vert_normal_indices(FormatHandler<eFileType::OBJ> &fh, + const IndexOffsets &offsets, + Span<int> vert_indices, Span<int> /*uv_indices*/, Span<int> normal_indices) const { BLI_assert(vert_indices.size() == normal_indices.size()); - file_handler_->write<eOBJSyntaxElement::poly_element_begin>(); + fh.write<eOBJSyntaxElement::poly_element_begin>(); for (int j = 0; j < vert_indices.size(); j++) { - file_handler_->write<eOBJSyntaxElement::vertex_normal_indices>( - vert_indices[j] + index_offsets_.vertex_offset + 1, - normal_indices[j] + index_offsets_.normal_offset + 1); + fh.write<eOBJSyntaxElement::vertex_normal_indices>(vert_indices[j] + offsets.vertex_offset + 1, + normal_indices[j] + offsets.normal_offset + + 1); } - file_handler_->write<eOBJSyntaxElement::poly_element_end>(); + fh.write<eOBJSyntaxElement::poly_element_end>(); } -void OBJWriter::write_vert_uv_indices(Span<int> vert_indices, +void OBJWriter::write_vert_uv_indices(FormatHandler<eFileType::OBJ> &fh, + const IndexOffsets &offsets, + Span<int> vert_indices, Span<int> uv_indices, Span<int> /*normal_indices*/) const { BLI_assert(vert_indices.size() == uv_indices.size()); - file_handler_->write<eOBJSyntaxElement::poly_element_begin>(); + fh.write<eOBJSyntaxElement::poly_element_begin>(); for (int j = 0; j < vert_indices.size(); j++) { - file_handler_->write<eOBJSyntaxElement::vertex_uv_indices>( - vert_indices[j] + index_offsets_.vertex_offset + 1, - uv_indices[j] + index_offsets_.uv_vertex_offset + 1); + fh.write<eOBJSyntaxElement::vertex_uv_indices>(vert_indices[j] + offsets.vertex_offset + 1, + uv_indices[j] + offsets.uv_vertex_offset + 1); } - file_handler_->write<eOBJSyntaxElement::poly_element_end>(); + fh.write<eOBJSyntaxElement::poly_element_end>(); } -void OBJWriter::write_vert_indices(Span<int> vert_indices, +void OBJWriter::write_vert_indices(FormatHandler<eFileType::OBJ> &fh, + const IndexOffsets &offsets, + Span<int> vert_indices, Span<int> /*uv_indices*/, Span<int> /*normal_indices*/) const { - file_handler_->write<eOBJSyntaxElement::poly_element_begin>(); + fh.write<eOBJSyntaxElement::poly_element_begin>(); for (const int vert_index : vert_indices) { - file_handler_->write<eOBJSyntaxElement::vertex_indices>(vert_index + - index_offsets_.vertex_offset + 1); + fh.write<eOBJSyntaxElement::vertex_indices>(vert_index + offsets.vertex_offset + 1); } - file_handler_->write<eOBJSyntaxElement::poly_element_end>(); + fh.write<eOBJSyntaxElement::poly_element_end>(); } void OBJWriter::write_header() const { using namespace std::string_literals; - file_handler_->write<eOBJSyntaxElement::string>("# Blender "s + BKE_blender_version_string() + - "\n"); - file_handler_->write<eOBJSyntaxElement::string>("# www.blender.org\n"); + FormatHandler<eFileType::OBJ> fh; + fh.write<eOBJSyntaxElement::string>("# Blender "s + BKE_blender_version_string() + "\n"); + fh.write<eOBJSyntaxElement::string>("# www.blender.org\n"); + fh.write_to_file(outfile_); } void OBJWriter::write_mtllib_name(const StringRefNull mtl_filepath) const @@ -122,10 +130,13 @@ void OBJWriter::write_mtllib_name(const StringRefNull mtl_filepath) const char mtl_file_name[FILE_MAXFILE]; char mtl_dir_name[FILE_MAXDIR]; BLI_split_dirfile(mtl_filepath.data(), mtl_dir_name, mtl_file_name, FILE_MAXDIR, FILE_MAXFILE); - file_handler_->write<eOBJSyntaxElement::mtllib>(mtl_file_name); + FormatHandler<eFileType::OBJ> fh; + fh.write<eOBJSyntaxElement::mtllib>(mtl_file_name); + fh.write_to_file(outfile_); } -void OBJWriter::write_object_group(const OBJMesh &obj_mesh_data) const +void OBJWriter::write_object_group(FormatHandler<eFileType::OBJ> &fh, + const OBJMesh &obj_mesh_data) const { /* "o object_name" is not mandatory. A valid .OBJ file may contain neither * "o name" nor "g group_name". */ @@ -138,125 +149,104 @@ void OBJWriter::write_object_group(const OBJMesh &obj_mesh_data) const const char *object_material_name = obj_mesh_data.get_object_material_name(0); if (export_params_.export_materials && export_params_.export_material_groups && object_material_name) { - file_handler_->write<eOBJSyntaxElement::object_group>(object_name + "_" + object_mesh_name + - "_" + object_material_name); - return; + fh.write<eOBJSyntaxElement::object_group>(object_name + "_" + object_mesh_name + "_" + + object_material_name); + } + else { + fh.write<eOBJSyntaxElement::object_group>(object_name + "_" + object_mesh_name); } - file_handler_->write<eOBJSyntaxElement::object_group>(object_name + "_" + object_mesh_name); } -void OBJWriter::write_object_name(const OBJMesh &obj_mesh_data) const +void OBJWriter::write_object_name(FormatHandler<eFileType::OBJ> &fh, + const OBJMesh &obj_mesh_data) const { const char *object_name = obj_mesh_data.get_object_name(); if (export_params_.export_object_groups) { - write_object_group(obj_mesh_data); + write_object_group(fh, obj_mesh_data); return; } - file_handler_->write<eOBJSyntaxElement::object_name>(object_name); + fh.write<eOBJSyntaxElement::object_name>(object_name); } -void OBJWriter::write_vertex_coords(const OBJMesh &obj_mesh_data) const +/* 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) { - const int tot_vertices = obj_mesh_data.tot_vertices(); - for (int i = 0; i < tot_vertices; i++) { - float3 vertex = obj_mesh_data.calc_vertex_coords(i, export_params_.scaling_factor); - file_handler_->write<eOBJSyntaxElement::vertex_coords>(vertex[0], vertex[1], vertex[2]); - } + return (count + chunk_size - 1) / chunk_size; } -void OBJWriter::write_uv_coords(OBJMesh &r_obj_mesh_data) const +/* 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<eFileType::OBJ> &fh, + int tot_count, + const Function &function) { - Vector<std::array<float, 2>> uv_coords; - /* UV indices are calculated and stored in an OBJMesh member here. */ - r_obj_mesh_data.store_uv_coords_and_indices(uv_coords); - - for (const std::array<float, 2> &uv_vertex : uv_coords) { - file_handler_->write<eOBJSyntaxElement::uv_vertex_coords>(uv_vertex[0], uv_vertex[1]); + if (tot_count <= 0) { + return; } -} - -void OBJWriter::write_poly_normals(OBJMesh &obj_mesh_data) -{ - obj_mesh_data.ensure_mesh_normals(); - Vector<float3> normals; - obj_mesh_data.store_normal_coords_and_indices(normals); - for (const float3 &normal : normals) { - file_handler_->write<eOBJSyntaxElement::normal>(normal[0], normal[1], normal[2]); + /* 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. */ + std::vector<FormatHandler<eFileType::OBJ>> buffers(chunk_count); + blender::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); } } -int OBJWriter::write_smooth_group(const OBJMesh &obj_mesh_data, - const int poly_index, - const int last_poly_smooth_group) const +void OBJWriter::write_vertex_coords(FormatHandler<eFileType::OBJ> &fh, + const OBJMesh &obj_mesh_data) const { - int current_group = SMOOTH_GROUP_DISABLED; - if (!export_params_.export_smooth_groups && obj_mesh_data.is_ith_poly_smooth(poly_index)) { - /* Smooth group calculation is disabled, but polygon is smooth-shaded. */ - current_group = SMOOTH_GROUP_DEFAULT; - } - else if (obj_mesh_data.is_ith_poly_smooth(poly_index)) { - /* Smooth group calc is enabled and polygon is smoothāshaded, so find the group. */ - current_group = obj_mesh_data.ith_smooth_group(poly_index); - } - - if (current_group == last_poly_smooth_group) { - /* Group has already been written, even if it is "s 0". */ - return current_group; - } - file_handler_->write<eOBJSyntaxElement::smooth_group>(current_group); - return current_group; + const int tot_count = obj_mesh_data.tot_vertices(); + obj_parallel_chunked_output(fh, tot_count, [&](FormatHandler<eFileType::OBJ> &buf, int i) { + float3 vertex = obj_mesh_data.calc_vertex_coords(i, export_params_.scaling_factor); + buf.write<eOBJSyntaxElement::vertex_coords>(vertex[0], vertex[1], vertex[2]); + }); } -int16_t OBJWriter::write_poly_material(const OBJMesh &obj_mesh_data, - const int poly_index, - const int16_t last_poly_mat_nr, - std::function<const char *(int)> matname_fn) const +void OBJWriter::write_uv_coords(FormatHandler<eFileType::OBJ> &fh, OBJMesh &r_obj_mesh_data) const { - if (!export_params_.export_materials || obj_mesh_data.tot_materials() <= 0) { - return last_poly_mat_nr; - } - const int16_t current_mat_nr = obj_mesh_data.ith_poly_matnr(poly_index); - /* Whenever a polygon with a new material is encountered, write its material - * and/or group, otherwise pass. */ - if (last_poly_mat_nr == current_mat_nr) { - return current_mat_nr; - } - if (current_mat_nr == NOT_FOUND) { - file_handler_->write<eOBJSyntaxElement::poly_usemtl>(MATERIAL_GROUP_DISABLED); - return current_mat_nr; - } - if (export_params_.export_object_groups) { - write_object_group(obj_mesh_data); - } - const char *mat_name = matname_fn(current_mat_nr); - if (!mat_name) { - mat_name = MATERIAL_GROUP_DISABLED; - } - file_handler_->write<eOBJSyntaxElement::poly_usemtl>(mat_name); - - return current_mat_nr; + const Vector<float2> &uv_coords = r_obj_mesh_data.get_uv_coords(); + const int tot_count = uv_coords.size(); + obj_parallel_chunked_output(fh, tot_count, [&](FormatHandler<eFileType::OBJ> &buf, int i) { + const float2 &uv_vertex = uv_coords[i]; + buf.write<eOBJSyntaxElement::uv_vertex_coords>(uv_vertex[0], uv_vertex[1]); + }); } -int16_t OBJWriter::write_vertex_group(const OBJMesh &obj_mesh_data, - const int poly_index, - const int16_t last_poly_vertex_group) const +void OBJWriter::write_poly_normals(FormatHandler<eFileType::OBJ> &fh, OBJMesh &obj_mesh_data) { - if (!export_params_.export_vertex_groups) { - return last_poly_vertex_group; - } - const int16_t current_group = obj_mesh_data.get_poly_deform_group_index(poly_index); - - if (current_group == last_poly_vertex_group) { - /* No vertex group found in this polygon, just like in the last iteration. */ - return current_group; - } - if (current_group == NOT_FOUND) { - file_handler_->write<eOBJSyntaxElement::object_group>(DEFORM_GROUP_DISABLED); - return current_group; - } - file_handler_->write<eOBJSyntaxElement::object_group>( - obj_mesh_data.get_poly_deform_group_name(current_group)); - return current_group; + /* Poly normals should be calculated earlier via store_normal_coords_and_indices. */ + const Vector<float3> &normal_coords = obj_mesh_data.get_normal_coords(); + const int tot_count = normal_coords.size(); + obj_parallel_chunked_output(fh, tot_count, [&](FormatHandler<eFileType::OBJ> &buf, int i) { + const float3 &normal = normal_coords[i]; + buf.write<eOBJSyntaxElement::normal>(normal[0], normal[1], normal[2]); + }); } OBJWriter::func_vert_uv_normal_indices OBJWriter::get_poly_element_writer( @@ -278,32 +268,85 @@ OBJWriter::func_vert_uv_normal_indices OBJWriter::get_poly_element_writer( return &OBJWriter::write_vert_indices; } -void OBJWriter::write_poly_elements(const OBJMesh &obj_mesh_data, - std::function<const char *(int)> matname_fn) +static int get_smooth_group(const OBJMesh &mesh, const OBJExportParams ¶ms, int poly_idx) { - int last_poly_smooth_group = NEGATIVE_INIT; - int16_t last_poly_vertex_group = NEGATIVE_INIT; - int16_t last_poly_mat_nr = NEGATIVE_INIT; + if (poly_idx < 0) { + return NEGATIVE_INIT; + } + int group = SMOOTH_GROUP_DISABLED; + if (mesh.is_ith_poly_smooth(poly_idx)) { + group = !params.export_smooth_groups ? SMOOTH_GROUP_DEFAULT : mesh.ith_smooth_group(poly_idx); + } + return group; +} +void OBJWriter::write_poly_elements(FormatHandler<eFileType::OBJ> &fh, + const IndexOffsets &offsets, + const OBJMesh &obj_mesh_data, + std::function<const char *(int)> matname_fn) +{ const func_vert_uv_normal_indices poly_element_writer = get_poly_element_writer( obj_mesh_data.tot_uv_vertices()); const int tot_polygons = obj_mesh_data.tot_polygons(); - for (int i = 0; i < tot_polygons; i++) { + obj_parallel_chunked_output(fh, tot_polygons, [&](FormatHandler<eFileType::OBJ> &buf, int i) { Vector<int> poly_vertex_indices = obj_mesh_data.calc_poly_vertex_indices(i); Span<int> poly_uv_indices = obj_mesh_data.calc_poly_uv_indices(i); Vector<int> poly_normal_indices = obj_mesh_data.calc_poly_normal_indices(i); - last_poly_smooth_group = write_smooth_group(obj_mesh_data, i, last_poly_smooth_group); - last_poly_vertex_group = write_vertex_group(obj_mesh_data, i, last_poly_vertex_group); - last_poly_mat_nr = write_poly_material(obj_mesh_data, i, last_poly_mat_nr, matname_fn); - (this->*poly_element_writer)(poly_vertex_indices, poly_uv_indices, poly_normal_indices); - } + /* Write smoothing group if different from previous. */ + { + const int prev_group = get_smooth_group(obj_mesh_data, export_params_, i - 1); + const int group = get_smooth_group(obj_mesh_data, export_params_, i); + if (group != prev_group) { + buf.write<eOBJSyntaxElement::smooth_group>(group); + } + } + + /* Write vertex group if different from previous. */ + if (export_params_.export_vertex_groups) { + const int16_t prev_group = i == 0 ? NEGATIVE_INIT : + obj_mesh_data.get_poly_deform_group_index(i - 1); + const int16_t group = obj_mesh_data.get_poly_deform_group_index(i); + if (group != prev_group) { + buf.write<eOBJSyntaxElement::object_group>( + group == NOT_FOUND ? DEFORM_GROUP_DISABLED : + obj_mesh_data.get_poly_deform_group_name(group)); + } + } + + /* Write material name and material group if different from previous. */ + if (export_params_.export_materials && obj_mesh_data.tot_materials() > 0) { + const int16_t prev_mat = i == 0 ? NEGATIVE_INIT : obj_mesh_data.ith_poly_matnr(i - 1); + const int16_t mat = obj_mesh_data.ith_poly_matnr(i); + if (mat != prev_mat) { + if (mat == NOT_FOUND) { + buf.write<eOBJSyntaxElement::poly_usemtl>(MATERIAL_GROUP_DISABLED); + } + else { + if (export_params_.export_object_groups) { + write_object_group(buf, obj_mesh_data); + } + const char *mat_name = matname_fn(mat); + if (!mat_name) { + mat_name = MATERIAL_GROUP_DISABLED; + } + buf.write<eOBJSyntaxElement::poly_usemtl>(mat_name); + } + } + } + + /* Write polygon elements. */ + (this->*poly_element_writer)( + buf, offsets, poly_vertex_indices, poly_uv_indices, poly_normal_indices); + }); } -void OBJWriter::write_edges_indices(const OBJMesh &obj_mesh_data) const +void OBJWriter::write_edges_indices(FormatHandler<eFileType::OBJ> &fh, + const IndexOffsets &offsets, + const OBJMesh &obj_mesh_data) const { - obj_mesh_data.ensure_mesh_edges(); + /* Note: ensure_mesh_edges should be called before. */ const int tot_edges = obj_mesh_data.tot_edges(); for (int edge_index = 0; edge_index < tot_edges; edge_index++) { const std::optional<std::array<int, 2>> vertex_indices = @@ -311,13 +354,13 @@ void OBJWriter::write_edges_indices(const OBJMesh &obj_mesh_data) const if (!vertex_indices) { continue; } - file_handler_->write<eOBJSyntaxElement::edge>( - (*vertex_indices)[0] + index_offsets_.vertex_offset + 1, - (*vertex_indices)[1] + index_offsets_.vertex_offset + 1); + fh.write<eOBJSyntaxElement::edge>((*vertex_indices)[0] + offsets.vertex_offset + 1, + (*vertex_indices)[1] + offsets.vertex_offset + 1); } } -void OBJWriter::write_nurbs_curve(const OBJCurve &obj_nurbs_data) const +void OBJWriter::write_nurbs_curve(FormatHandler<eFileType::OBJ> &fh, + const OBJCurve &obj_nurbs_data) const { const int total_splines = obj_nurbs_data.total_splines(); for (int spline_idx = 0; spline_idx < total_splines; spline_idx++) { @@ -325,15 +368,15 @@ void OBJWriter::write_nurbs_curve(const OBJCurve &obj_nurbs_data) const for (int vertex_idx = 0; vertex_idx < total_vertices; vertex_idx++) { const float3 vertex_coords = obj_nurbs_data.vertex_coordinates( spline_idx, vertex_idx, export_params_.scaling_factor); - file_handler_->write<eOBJSyntaxElement::vertex_coords>( + fh.write<eOBJSyntaxElement::vertex_coords>( vertex_coords[0], vertex_coords[1], vertex_coords[2]); } const char *nurbs_name = obj_nurbs_data.get_curve_name(); const int nurbs_degree = obj_nurbs_data.get_nurbs_degree(spline_idx); - file_handler_->write<eOBJSyntaxElement::object_group>(nurbs_name); - file_handler_->write<eOBJSyntaxElement::cstype>(); - file_handler_->write<eOBJSyntaxElement::nurbs_degree>(nurbs_degree); + fh.write<eOBJSyntaxElement::object_group>(nurbs_name); + fh.write<eOBJSyntaxElement::cstype>(); + fh.write<eOBJSyntaxElement::nurbs_degree>(nurbs_degree); /** * The numbers written here are indices into the vertex coordinates written * earlier, relative to the line that is going to be written. @@ -342,36 +385,42 @@ void OBJWriter::write_nurbs_curve(const OBJCurve &obj_nurbs_data) const * 0.0 1.0 -1 -2 -3 -4 -1 -2 -3 for a cyclic curve with 4 vertices. */ const int total_control_points = obj_nurbs_data.total_spline_control_points(spline_idx); - file_handler_->write<eOBJSyntaxElement::curve_element_begin>(); + fh.write<eOBJSyntaxElement::curve_element_begin>(); for (int i = 0; i < total_control_points; i++) { /* "+1" to keep indices one-based, even if they're negative: i.e., -1 refers to the * last vertex coordinate, -2 second last. */ - file_handler_->write<eOBJSyntaxElement::vertex_indices>(-((i % total_vertices) + 1)); + fh.write<eOBJSyntaxElement::vertex_indices>(-((i % total_vertices) + 1)); } - file_handler_->write<eOBJSyntaxElement::curve_element_end>(); + fh.write<eOBJSyntaxElement::curve_element_end>(); /** * In `parm u 0 0.1 ..` line:, (total control points + 2) equidistant numbers in the - * parameter range are inserted. + * parameter range are inserted. However for curves with endpoint flag, + * first degree+1 numbers are zeroes, and last degree+1 numbers are ones */ - file_handler_->write<eOBJSyntaxElement::nurbs_parameter_begin>(); + + const short flagsu = obj_nurbs_data.get_nurbs_flagu(spline_idx); + const bool cyclic = flagsu & CU_NURB_CYCLIC; + const bool endpoint = !cyclic && (flagsu & CU_NURB_ENDPOINT); + fh.write<eOBJSyntaxElement::nurbs_parameter_begin>(); for (int i = 1; i <= total_control_points + 2; i++) { - file_handler_->write<eOBJSyntaxElement::nurbs_parameters>(1.0f * i / - (total_control_points + 2 + 1)); + float parm = 1.0f * i / (total_control_points + 2 + 1); + if (endpoint) { + if (i <= nurbs_degree) { + parm = 0; + } + else if (i > total_control_points + 2 - nurbs_degree) { + parm = 1; + } + } + fh.write<eOBJSyntaxElement::nurbs_parameters>(parm); } - file_handler_->write<eOBJSyntaxElement::nurbs_parameter_end>(); + fh.write<eOBJSyntaxElement::nurbs_parameter_end>(); - file_handler_->write<eOBJSyntaxElement::nurbs_group_end>(); + fh.write<eOBJSyntaxElement::nurbs_group_end>(); } } -void OBJWriter::update_index_offsets(const OBJMesh &obj_mesh_data) -{ - index_offsets_.vertex_offset += obj_mesh_data.tot_vertices(); - index_offsets_.uv_vertex_offset += obj_mesh_data.tot_uv_vertices(); - index_offsets_.normal_offset += obj_mesh_data.tot_normal_indices(); -} - /* -------------------------------------------------------------------- */ /** \name .MTL writers. * \{ */ @@ -394,18 +443,31 @@ MTLWriter::MTLWriter(const char *obj_filepath) noexcept(false) if (!ok) { throw std::system_error(ENAMETOOLONG, std::system_category(), ""); } - file_handler_ = std::make_unique<FormattedFileHandler<eFileType::MTL>>(mtl_filepath_); + 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_)) { + std::cerr << "Error: could not close the file '" << mtl_filepath_ + << "' properly, it may be corrupted." << std::endl; + } + } } -void MTLWriter::write_header(const char *blen_filepath) const +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"; - file_handler_->write<eMTLSyntaxElement::string>("# Blender "s + BKE_blender_version_string() + - " MTL File: '" + blen_basename + "'\n"); - file_handler_->write<eMTLSyntaxElement::string>("# www.blender.org\n"); + fmt_handler_.write<eMTLSyntaxElement::string>("# Blender "s + BKE_blender_version_string() + + " MTL File: '" + blen_basename + "'\n"); + fmt_handler_.write<eMTLSyntaxElement::string>("# www.blender.org\n"); } StringRefNull MTLWriter::mtl_file_path() const @@ -415,18 +477,18 @@ StringRefNull MTLWriter::mtl_file_path() const void MTLWriter::write_bsdf_properties(const MTLMaterial &mtl_material) { - file_handler_->write<eMTLSyntaxElement::Ns>(mtl_material.Ns); - file_handler_->write<eMTLSyntaxElement::Ka>( + fmt_handler_.write<eMTLSyntaxElement::Ns>(mtl_material.Ns); + fmt_handler_.write<eMTLSyntaxElement::Ka>( mtl_material.Ka.x, mtl_material.Ka.y, mtl_material.Ka.z); - file_handler_->write<eMTLSyntaxElement::Kd>( + fmt_handler_.write<eMTLSyntaxElement::Kd>( mtl_material.Kd.x, mtl_material.Kd.y, mtl_material.Kd.z); - file_handler_->write<eMTLSyntaxElement::Ks>( + fmt_handler_.write<eMTLSyntaxElement::Ks>( mtl_material.Ks.x, mtl_material.Ks.y, mtl_material.Ks.z); - file_handler_->write<eMTLSyntaxElement::Ke>( + fmt_handler_.write<eMTLSyntaxElement::Ke>( mtl_material.Ke.x, mtl_material.Ke.y, mtl_material.Ke.z); - file_handler_->write<eMTLSyntaxElement::Ni>(mtl_material.Ni); - file_handler_->write<eMTLSyntaxElement::d>(mtl_material.d); - file_handler_->write<eMTLSyntaxElement::illum>(mtl_material.illum); + fmt_handler_.write<eMTLSyntaxElement::Ni>(mtl_material.Ni); + fmt_handler_.write<eMTLSyntaxElement::d>(mtl_material.d); + fmt_handler_.write<eMTLSyntaxElement::illum>(mtl_material.illum); } void MTLWriter::write_texture_map( @@ -449,8 +511,8 @@ void MTLWriter::write_texture_map( #define SYNTAX_DISPATCH(eMTLSyntaxElement) \ if (texture_map.key == eMTLSyntaxElement) { \ - file_handler_->write<eMTLSyntaxElement>(translation + scale + map_bump_strength, \ - texture_map.value.image_path); \ + fmt_handler_.write<eMTLSyntaxElement>(translation + scale + map_bump_strength, \ + texture_map.value.image_path); \ return; \ } @@ -474,8 +536,8 @@ void MTLWriter::write_materials() mtlmaterials_.end(), [](const MTLMaterial &a, const MTLMaterial &b) { return a.name < b.name; }); for (const MTLMaterial &mtlmat : mtlmaterials_) { - file_handler_->write<eMTLSyntaxElement::string>("\n"); - file_handler_->write<eMTLSyntaxElement::newmtl>(mtlmat.name); + fmt_handler_.write<eMTLSyntaxElement::string>("\n"); + fmt_handler_.write<eMTLSyntaxElement::newmtl>(mtlmat.name); write_bsdf_properties(mtlmat); for (const Map<const eMTLSyntaxElement, tex_map_XX>::Item &texture_map : mtlmat.texture_maps.items()) { diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh index 7385d9fabe2..c88955e5090 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh +++ b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh @@ -49,14 +49,29 @@ struct IndexOffsets { class OBJWriter : NonMovable, NonCopyable { private: const OBJExportParams &export_params_; - std::unique_ptr<FormattedFileHandler<eFileType::OBJ>> file_handler_ = nullptr; - IndexOffsets index_offsets_{0, 0, 0}; + std::string outfile_path_; + FILE *outfile_; public: OBJWriter(const char *filepath, const OBJExportParams &export_params) noexcept(false) - : export_params_(export_params) + : export_params_(export_params), outfile_path_(filepath), outfile_(nullptr) { - file_handler_ = std::make_unique<FormattedFileHandler<eFileType::OBJ>>(filepath); + outfile_ = BLI_fopen(filepath, "wb"); + if (!outfile_) { + throw std::system_error(errno, std::system_category(), "Cannot open file " + outfile_path_); + } + } + ~OBJWriter() + { + if (outfile_ && std::fclose(outfile_)) { + std::cerr << "Error: could not close the file '" << outfile_path_ + << "' properly, it may be corrupted." << std::endl; + } + } + + FILE *get_outfile() const + { + return outfile_; } void write_header() const; @@ -64,11 +79,11 @@ class OBJWriter : NonMovable, NonCopyable { /** * Write object's name or group. */ - void write_object_name(const OBJMesh &obj_mesh_data) const; + void write_object_name(FormatHandler<eFileType::OBJ> &fh, const OBJMesh &obj_mesh_data) const; /** * Write an object's group with mesh and/or material name appended conditionally. */ - void write_object_group(const OBJMesh &obj_mesh_data) const; + void write_object_group(FormatHandler<eFileType::OBJ> &fh, const OBJMesh &obj_mesh_data) const; /** * Write file name of Material Library in .OBJ file. */ @@ -76,38 +91,17 @@ class OBJWriter : NonMovable, NonCopyable { /** * Write vertex coordinates for all vertices as "v x y z". */ - void write_vertex_coords(const OBJMesh &obj_mesh_data) const; + void write_vertex_coords(FormatHandler<eFileType::OBJ> &fh, const OBJMesh &obj_mesh_data) const; /** * Write UV vertex coordinates for all vertices as `vt u v`. * \note UV indices are stored here, but written with polygons later. */ - void write_uv_coords(OBJMesh &obj_mesh_data) const; + void write_uv_coords(FormatHandler<eFileType::OBJ> &fh, OBJMesh &obj_mesh_data) const; /** * Write loop normals for smooth-shaded polygons, and polygon normals otherwise, as "vn x y z". * \note Normal indices ares stored here, but written with polygons later. */ - void write_poly_normals(OBJMesh &obj_mesh_data); - /** - * Write smooth group if polygon at the given index is shaded smooth else "s 0" - */ - int write_smooth_group(const OBJMesh &obj_mesh_data, - int poly_index, - int last_poly_smooth_group) const; - /** - * Write material name and material group of a polygon in the .OBJ file. - * \return #mat_nr of the polygon at the given index. - * \note It doesn't write to the material library. - */ - int16_t write_poly_material(const OBJMesh &obj_mesh_data, - int poly_index, - int16_t last_poly_mat_nr, - std::function<const char *(int)> matname_fn) const; - /** - * Write the name of the deform group of a polygon. - */ - int16_t write_vertex_group(const OBJMesh &obj_mesh_data, - int poly_index, - int16_t last_poly_vertex_group) const; + void write_poly_normals(FormatHandler<eFileType::OBJ> &fh, OBJMesh &obj_mesh_data); /** * Write polygon elements with at least vertex indices, and conditionally with UV vertex * indices and polygon normal indices. Also write groups: smooth, vertex, material. @@ -115,25 +109,25 @@ class OBJWriter : NonMovable, NonCopyable { * name used in the .obj file. * \note UV indices were stored while writing UV vertices. */ - void write_poly_elements(const OBJMesh &obj_mesh_data, + void write_poly_elements(FormatHandler<eFileType::OBJ> &fh, + const IndexOffsets &offsets, + const OBJMesh &obj_mesh_data, std::function<const char *(int)> matname_fn); /** * Write loose edges of a mesh as "l v1 v2". */ - void write_edges_indices(const OBJMesh &obj_mesh_data) const; + void write_edges_indices(FormatHandler<eFileType::OBJ> &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(const OBJCurve &obj_nurbs_data) const; - - /** - * When there are multiple objects in a frame, the indices of previous objects' coordinates or - * normals add up. - */ - void update_index_offsets(const OBJMesh &obj_mesh_data); + void write_nurbs_curve(FormatHandler<eFileType::OBJ> &fh, const OBJCurve &obj_nurbs_data) const; private: - using func_vert_uv_normal_indices = void (OBJWriter::*)(Span<int> vert_indices, + using func_vert_uv_normal_indices = void (OBJWriter::*)(FormatHandler<eFileType::OBJ> &fh, + const IndexOffsets &offsets, + Span<int> vert_indices, Span<int> uv_indices, Span<int> normal_indices) const; /** @@ -144,25 +138,33 @@ class OBJWriter : NonMovable, NonCopyable { /** * Write one line of polygon indices as "f v1/vt1/vn1 v2/vt2/vn2 ...". */ - void write_vert_uv_normal_indices(Span<int> vert_indices, + void write_vert_uv_normal_indices(FormatHandler<eFileType::OBJ> &fh, + const IndexOffsets &offsets, + Span<int> vert_indices, Span<int> uv_indices, Span<int> normal_indices) const; /** * Write one line of polygon indices as "f v1//vn1 v2//vn2 ...". */ - void write_vert_normal_indices(Span<int> vert_indices, + void write_vert_normal_indices(FormatHandler<eFileType::OBJ> &fh, + const IndexOffsets &offsets, + Span<int> vert_indices, Span<int> /*uv_indices*/, Span<int> normal_indices) const; /** * Write one line of polygon indices as "f v1/vt1 v2/vt2 ...". */ - void write_vert_uv_indices(Span<int> vert_indices, + void write_vert_uv_indices(FormatHandler<eFileType::OBJ> &fh, + const IndexOffsets &offsets, + Span<int> vert_indices, Span<int> uv_indices, Span<int> /*normal_indices*/) const; /** * Write one line of polygon indices as "f v1 v2 ...". */ - void write_vert_indices(Span<int> vert_indices, + void write_vert_indices(FormatHandler<eFileType::OBJ> &fh, + const IndexOffsets &offsets, + Span<int> vert_indices, Span<int> /*uv_indices*/, Span<int> /*normal_indices*/) const; }; @@ -172,7 +174,8 @@ class OBJWriter : NonMovable, NonCopyable { */ class MTLWriter : NonMovable, NonCopyable { private: - std::unique_ptr<FormattedFileHandler<eFileType::MTL>> file_handler_ = nullptr; + FormatHandler<eFileType::MTL> fmt_handler_; + FILE *outfile_; std::string mtl_filepath_; Vector<MTLMaterial> mtlmaterials_; /* Map from a Material* to an index into mtlmaterials_. */ @@ -183,8 +186,9 @@ class MTLWriter : NonMovable, NonCopyable { * Create the .MTL file. */ MTLWriter(const char *obj_filepath) noexcept(false); + ~MTLWriter(); - void write_header(const char *blen_filepath) const; + 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), diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_io.hh b/source/blender/io/wavefront_obj/exporter/obj_export_io.hh index 1bbefaee75f..7d8427a8980 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_io.hh +++ b/source/blender/io/wavefront_obj/exporter/obj_export_io.hh @@ -24,6 +24,7 @@ #include <string> #include <system_error> #include <type_traits> +#include <vector> #include "BLI_compiler_attrs.h" #include "BLI_fileops.h" @@ -124,6 +125,14 @@ constexpr bool is_type_integral = (... && std::is_integral_v<std::decay_t<T>>); template<typename... T> constexpr bool is_type_string_related = (... && std::is_constructible_v<std::string, T>); +/* GCC (at least 9.3) while compiling the obj_exporter_tests.cc with optimizations on, + * results in "obj_export_io.hh:205:18: warning: ā%sā directive output truncated writing 34 bytes + * into a region of size 6" and similar warnings. Yes the output is truncated, and that is covered + * as an edge case by tests on purpose. */ +#if defined __GNUC__ +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wformat-truncation" +#endif template<typename... T> constexpr FormattingSyntax syntax_elem_to_formatting(const eOBJSyntaxElement key) { @@ -168,7 +177,7 @@ constexpr FormattingSyntax syntax_elem_to_formatting(const eOBJSyntaxElement key return {"curv 0.0 1.0", 0, is_type_string_related<T...>}; } case eOBJSyntaxElement::nurbs_parameter_begin: { - return {"parm 0.0", 0, is_type_string_related<T...>}; + return {"parm u 0.0", 0, is_type_string_related<T...>}; } case eOBJSyntaxElement::nurbs_parameters: { return {" %f", 1, is_type_float<T...>}; @@ -264,31 +273,52 @@ constexpr FormattingSyntax syntax_elem_to_formatting(const eMTLSyntaxElement key } } } +#if defined __GNUC__ +# pragma GCC diagnostic pop +#endif /** - * File format and syntax agnostic file writer. + * File format and syntax agnostic 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. */ -template<eFileType filetype> class FormattedFileHandler : NonCopyable, NonMovable { +template<eFileType filetype, + size_t buffer_chunk_size = 64 * 1024, + size_t write_local_buffer_size = 1024> +class FormatHandler : NonCopyable, NonMovable { private: - std::FILE *outfile_ = nullptr; - std::string outfile_path_; + typedef std::vector<char> VectorChar; + std::vector<VectorChar> blocks_; public: - FormattedFileHandler(std::string outfile_path) noexcept(false) - : outfile_path_(std::move(outfile_path)) + /* Write contents to the buffer(s) into a file, and clear the buffers. */ + void write_to_file(FILE *f) { - outfile_ = BLI_fopen(outfile_path_.c_str(), "w"); - if (!outfile_) { - throw std::system_error(errno, std::system_category(), "Cannot open file " + outfile_path_); - } + for (const auto &b : blocks_) + fwrite(b.data(), 1, b.size(), f); + blocks_.clear(); } - ~FormattedFileHandler() + std::string get_as_string() const { - if (outfile_ && std::fclose(outfile_)) { - std::cerr << "Error: could not close the file '" << outfile_path_ - << "' properly, it may be corrupted." << std::endl; - } + 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<filetype, buffer_chunk_size, write_local_buffer_size> &v) + { + blocks_.insert(blocks_.end(), + std::make_move_iterator(v.blocks_.begin()), + std::make_move_iterator(v.blocks_.end())); + v.blocks_.clear(); } /** @@ -298,7 +328,7 @@ template<eFileType filetype> class FormattedFileHandler : NonCopyable, NonMovabl * `eFileType::MTL`. */ template<typename FileTypeTraits<filetype>::SyntaxType key, typename... T> - constexpr void write(T &&...args) const + constexpr void write(T &&...args) { /* Get format syntax, number of arguments expected and whether types of given arguments are * valid. @@ -339,13 +369,47 @@ template<eFileType filetype> class FormattedFileHandler : NonCopyable, NonMovabl } } - template<typename... T> constexpr void write_impl(const char *fmt, T &&...args) const + /* 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_.empty() || (blocks_.back().capacity() - blocks_.back().size() < at_least)) { + VectorChar &b = blocks_.emplace_back(VectorChar()); + b.reserve(std::max(at_least, buffer_chunk_size)); + } + } + + template<typename... T> constexpr void write_impl(const char *fmt, T &&...args) { if constexpr (sizeof...(T) == 0) { - std::fputs(fmt, outfile_); + /* No arguments: just emit the format string. */ + size_t len = strlen(fmt); + ensure_space(len); + VectorChar &bb = blocks_.back(); + bb.insert(bb.end(), fmt, fmt + len); } else { - std::fprintf(outfile_, fmt, convert_to_primitive(std::forward<T>(args))...); + /* Format into a local buffer. */ + char buf[write_local_buffer_size]; + int needed = std::snprintf( + buf, write_local_buffer_size, fmt, convert_to_primitive(std::forward<T>(args))...); + if (needed < 0) + throw std::system_error( + errno, std::system_category(), "Failed to format obj export string into a buffer"); + ensure_space(needed + 1); /* Ensure space for zero terminator. */ + VectorChar &bb = blocks_.back(); + if (needed < write_local_buffer_size) { + /* String formatted successfully into the local buffer, copy it. */ + bb.insert(bb.end(), buf, buf + needed); + } + else { + /* Would need more space than the local buffer: insert said space and format again into + * that. */ + size_t bbEnd = bb.size(); + bb.insert(bb.end(), needed, ' '); + std::snprintf( + bb.data() + bbEnd, needed + 1, fmt, convert_to_primitive(std::forward<T>(args))...); + } } } }; diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc index c1b12ddd217..468631cdd82 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc +++ b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc @@ -69,16 +69,28 @@ OBJMesh::OBJMesh(Depsgraph *depsgraph, const OBJExportParams &export_params, Obj */ OBJMesh::~OBJMesh() { - free_mesh_if_needed(); - if (poly_smooth_groups_) { - MEM_freeN(poly_smooth_groups_); - } + clear(); } void OBJMesh::free_mesh_if_needed() { if (mesh_eval_needs_free_ && export_mesh_eval_) { BKE_id_free(nullptr, export_mesh_eval_); + export_mesh_eval_ = nullptr; + mesh_eval_needs_free_ = false; + } +} + +void OBJMesh::clear() +{ + free_mesh_if_needed(); + uv_indices_.clear_and_make_inline(); + uv_coords_.clear_and_make_inline(); + loop_to_normal_index_.clear_and_make_inline(); + normal_coords_.clear_and_make_inline(); + if (poly_smooth_groups_) { + MEM_freeN(poly_smooth_groups_); + poly_smooth_groups_ = nullptr; } } @@ -256,7 +268,7 @@ Vector<int> OBJMesh::calc_poly_vertex_indices(const int poly_index) const return r_poly_vertex_indices; } -void OBJMesh::store_uv_coords_and_indices(Vector<std::array<float, 2>> &r_uv_coords) +void OBJMesh::store_uv_coords_and_indices() { const MPoly *mpoly = export_mesh_eval_->mpoly; const MLoop *mloop = export_mesh_eval_->mloop; @@ -276,7 +288,7 @@ void OBJMesh::store_uv_coords_and_indices(Vector<std::array<float, 2>> &r_uv_coo uv_indices_.resize(totpoly); /* At least total vertices of a mesh will be present in its texture map. So * reserve minimum space early. */ - r_uv_coords.reserve(totvert); + uv_coords_.reserve(totvert); tot_uv_vertices_ = 0; for (int vertex_index = 0; vertex_index < totvert; vertex_index++) { @@ -288,11 +300,10 @@ void OBJMesh::store_uv_coords_and_indices(Vector<std::array<float, 2>> &r_uv_coo const int vertices_in_poly = mpoly[uv_vert->poly_index].totloop; /* Store UV vertex coordinates. */ - r_uv_coords.resize(tot_uv_vertices_); + uv_coords_.resize(tot_uv_vertices_); const int loopstart = mpoly[uv_vert->poly_index].loopstart; Span<float> vert_uv_coords(mloopuv[loopstart + uv_vert->loop_of_poly_index].uv, 2); - r_uv_coords[tot_uv_vertices_ - 1][0] = vert_uv_coords[0]; - r_uv_coords[tot_uv_vertices_ - 1][1] = vert_uv_coords[1]; + uv_coords_[tot_uv_vertices_ - 1] = float2(vert_uv_coords[0], vert_uv_coords[1]); /* Store UV vertex indices. */ uv_indices_[uv_vert->poly_index].resize(vertices_in_poly); @@ -340,7 +351,7 @@ static float3 round_float3_to_n_digits(const float3 &v, int round_digits) return ans; } -void OBJMesh::store_normal_coords_and_indices(Vector<float3> &r_normal_coords) +void OBJMesh::store_normal_coords_and_indices() { /* We'll round normal components to 4 digits. * This will cover up some minor differences @@ -358,7 +369,7 @@ void OBJMesh::store_normal_coords_and_indices(Vector<float3> &r_normal_coords) *lnors)[3] = (const float(*)[3])(CustomData_get_layer(&export_mesh_eval_->ldata, CD_NORMAL)); for (int poly_index = 0; poly_index < export_mesh_eval_->totpoly; ++poly_index) { const MPoly &mpoly = export_mesh_eval_->mpoly[poly_index]; - bool need_per_loop_normals = is_ith_poly_smooth(poly_index); + bool need_per_loop_normals = lnors != nullptr || (mpoly.flag & ME_SMOOTH); if (need_per_loop_normals) { for (int loop_of_poly = 0; loop_of_poly < mpoly.totloop; ++loop_of_poly) { float3 loop_normal; @@ -371,7 +382,7 @@ void OBJMesh::store_normal_coords_and_indices(Vector<float3> &r_normal_coords) if (loop_norm_index == -1) { loop_norm_index = cur_normal_index++; normal_to_index.add(rounded_loop_normal, loop_norm_index); - r_normal_coords.append(rounded_loop_normal); + normal_coords_.append(rounded_loop_normal); } loop_to_normal_index_[loop_index] = loop_norm_index; } @@ -383,7 +394,7 @@ void OBJMesh::store_normal_coords_and_indices(Vector<float3> &r_normal_coords) if (poly_norm_index == -1) { poly_norm_index = cur_normal_index++; normal_to_index.add(rounded_poly_normal, poly_norm_index); - r_normal_coords.append(rounded_poly_normal); + normal_coords_.append(rounded_poly_normal); } for (int i = 0; i < mpoly.totloop; ++i) { int loop_index = mpoly.loopstart + i; diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh index f3ace140006..4cfbffdcebc 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh +++ b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh @@ -82,11 +82,19 @@ class OBJMesh : NonCopyable { * Per-polygon-per-vertex UV vertex indices. */ Vector<Vector<int>> uv_indices_; + /* + * UV vertices. + */ + Vector<float2> uv_coords_; /** * Per-loop normal index. */ Vector<int> loop_to_normal_index_; /* + * Normal coords. + */ + Vector<float3> normal_coords_; + /* * Total number of normal indices (maximum entry, plus 1, in * the loop_to_norm_index_ vector). */ @@ -108,6 +116,9 @@ class OBJMesh : NonCopyable { 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_polygons() const; int tot_uv_vertices() const; @@ -165,10 +176,14 @@ class OBJMesh : NonCopyable { Vector<int> calc_poly_vertex_indices(int poly_index) const; /** * Calculate UV vertex coordinates of an Object. - * - * \note Also store the UV vertex indices in the member variable. + * Stores the coordinates and UV vertex indices in the member variables. */ - void store_uv_coords_and_indices(Vector<std::array<float, 2>> &r_uv_coords); + void store_uv_coords_and_indices(); + /* Get UV coordinates computed by store_uv_coords_and_indices. */ + const Vector<float2> &get_uv_coords() const + { + return uv_coords_; + } Span<int> calc_poly_uv_indices(int poly_index) const; /** * Calculate polygon normal of a polygon at given index. @@ -177,10 +192,15 @@ class OBJMesh : NonCopyable { */ float3 calc_poly_normal(int poly_index) const; /** - * Find the unique normals of the mesh and return them in \a r_normal_coords. - * Store the indices into that vector with for each loop in this #OBJMesh. + * Find the unique normals of the mesh and stores them in a member variable. + * Also stores the indices into that vector with for each loop. */ - void store_normal_coords_and_indices(Vector<float3> &r_normal_coords); + void store_normal_coords_and_indices(); + /* Get normals calculate by store_normal_coords_and_indices. */ + const Vector<float3> &get_normal_coords() const + { + return normal_coords_; + } /** * Calculate a polygon's polygon/loop normal indices. * \param poly_index Index of the polygon to calculate indices for. diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_mtl.cc b/source/blender/io/wavefront_obj/exporter/obj_export_mtl.cc index 48136dad5f7..3637a3f3c5f 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_mtl.cc +++ b/source/blender/io/wavefront_obj/exporter/obj_export_mtl.cc @@ -193,8 +193,8 @@ static void store_bsdf_properties(const nodes::NodeRef *bsdf_node, copy_property_from_node(SOCK_FLOAT, bnode, "Roughness", {&roughness, 1}); } /* Empirical approximation. Importer should use the inverse of this method. */ - float spec_exponent = (1.0f - roughness) * 30; - spec_exponent *= spec_exponent; + float spec_exponent = (1.0f - roughness); + spec_exponent *= spec_exponent * 1000.0f; float specular = material->spec; if (bnode) { diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc index ec690115115..8a6d3b4b93a 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc +++ b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc @@ -102,4 +102,10 @@ int OBJCurve::get_nurbs_degree(const int spline_index) const return nurb->orderu - 1; } +short OBJCurve::get_nurbs_flagu(const int spline_index) const +{ + const Nurb *const nurb = static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index)); + return nurb->flagu; +} + } // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh index 0c71c3cc09d..d831afec0a0 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh +++ b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh @@ -61,6 +61,10 @@ class OBJCurve : NonCopyable { * Get the degree of the NURBS spline at the given index. */ int get_nurbs_degree(int spline_index) const; + /** + * Get the U flags (CU_NURB_*) of the NURBS spline at the given index. + */ + short get_nurbs_flagu(int spline_index) const; private: /** diff --git a/source/blender/io/wavefront_obj/exporter/obj_exporter.cc b/source/blender/io/wavefront_obj/exporter/obj_exporter.cc index 0c753ccdcac..187f50277f1 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_exporter.cc +++ b/source/blender/io/wavefront_obj/exporter/obj_exporter.cc @@ -25,6 +25,7 @@ #include "BKE_scene.h" #include "BLI_path_util.h" +#include "BLI_task.hh" #include "BLI_vector.hh" #include "DEG_depsgraph_query.h" @@ -155,44 +156,97 @@ static void write_mesh_objects(Vector<std::unique_ptr<OBJMesh>> exportable_as_me 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(); + std::vector<FormatHandler<eFileType::OBJ>> buffers(count); + + /* Serial: gather material indices, ensure normals & edges. */ + Vector<Vector<int>> mtlindices; if (mtl_writer) { obj_writer.write_mtllib_name(mtl_writer->mtl_file_path()); + mtlindices.reserve(count); } - - /* Smooth groups and UV vertex indices may make huge memory allocations, so they should be freed - * right after they're written, instead of waiting for #blender::Vector to clean them up after - * all the objects are exported. */ for (auto &obj_mesh : exportable_as_mesh) { - obj_writer.write_object_name(*obj_mesh); - obj_writer.write_vertex_coords(*obj_mesh); - Vector<int> obj_mtlindices; + OBJMesh &obj = *obj_mesh; + if (mtl_writer) { + mtlindices.append(mtl_writer->add_materials(obj)); + } + if (export_params.export_normals) { + obj.ensure_mesh_normals(); + } + obj.ensure_mesh_edges(); + } - if (obj_mesh->tot_polygons() > 0) { - if (export_params.export_smooth_groups) { - obj_mesh->calc_smooth_groups(export_params.smooth_groups_bitflags); - } + /* Parallel over meshes: store normal coords & indices, uv coords and indices. */ + blender::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_writer.write_poly_normals(*obj_mesh); + obj.store_normal_coords_and_indices(); } if (export_params.export_uv) { - obj_writer.write_uv_coords(*obj_mesh); - } - if (mtl_writer) { - obj_mtlindices = mtl_writer->add_materials(*obj_mesh); + obj.store_uv_coords_and_indices(); } - /* 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. */ - std::function<const char *(int)> matname_fn = [&](int s) -> const char * { - if (!mtl_writer || s < 0 || s >= obj_mtlindices.size()) { - return nullptr; + } + }); + + /* 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 (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.tot_normal_indices(); + } + + /* Parallel over meshes: main result writing. */ + blender::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); + + if (obj.tot_polygons() > 0) { + if (export_params.export_smooth_groups) { + obj.calc_smooth_groups(export_params.smooth_groups_bitflags); + } + if (export_params.export_normals) { + obj_writer.write_poly_normals(fh, obj); } - return mtl_writer->mtlmaterial_name(obj_mtlindices[s]); - }; - obj_writer.write_poly_elements(*obj_mesh, matname_fn); + 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]; + std::function<const char *(int)> 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_poly_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(); } - obj_writer.write_edges_indices(*obj_mesh); + }); - obj_writer.update_index_offsets(*obj_mesh); + /* 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); } } @@ -202,11 +256,13 @@ static void write_mesh_objects(Vector<std::unique_ptr<OBJMesh>> exportable_as_me static void write_nurbs_curve_objects(const Vector<std::unique_ptr<OBJCurve>> &exportable_as_nurbs, const OBJWriter &obj_writer) { + FormatHandler<eFileType::OBJ> fh; /* #OBJCurve doesn't have any dynamically allocated memory, so it's fine * to wait for #blender::Vector to clean the objects up. */ for (const std::unique_ptr<OBJCurve> &obj_curve : exportable_as_nurbs) { - obj_writer.write_nurbs_curve(*obj_curve); + obj_writer.write_nurbs_curve(fh, *obj_curve); } + fh.write_to_file(obj_writer.get_outfile()); } void export_frame(Depsgraph *depsgraph, const OBJExportParams &export_params, const char *filepath) diff --git a/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc index 89e1de49511..58329f63650 100644 --- a/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc +++ b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc @@ -193,7 +193,7 @@ static std::string read_temp_file_in_string(const std::string &file_path) std::string res; size_t buffer_len; void *buffer = BLI_file_read_text_as_mem(file_path.c_str(), 0, &buffer_len); - if (buffer != NULL) { + if (buffer != nullptr) { res.assign((const char *)buffer, buffer_len); MEM_freeN(buffer); } @@ -238,6 +238,38 @@ TEST(obj_exporter_writer, mtllib) BLI_delete(out_file_path.c_str(), false, false); } +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<eFileType::OBJ, 16, 8> h; + h.write<eOBJSyntaxElement::object_name>("abc"); + h.write<eOBJSyntaxElement::object_name>("abcd"); + h.write<eOBJSyntaxElement::object_name>("abcde"); + h.write<eOBJSyntaxElement::object_name>("abcdef"); + h.write<eOBJSyntaxElement::object_name>("012345678901234567890123456789abcd"); + h.write<eOBJSyntaxElement::object_name>("123"); + h.write<eOBJSyntaxElement::curve_element_begin>(); + h.write<eOBJSyntaxElement::new_line>(); + h.write<eOBJSyntaxElement::nurbs_parameter_begin>(); + h.write<eOBJSyntaxElement::new_line>(); + + size_t got_blocks = h.get_block_count(); + ASSERT_EQ(got_blocks, 7); + + 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 0.0 1.0 +parm u 0.0 +)"; + 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) { @@ -313,8 +345,14 @@ class obj_exporter_regression_test : public obj_exporter_test { std::string output_mtl_str = read_temp_file_in_string(out_mtl_file_path); std::string golden_mtl_file_path = blender::tests::flags_test_asset_dir() + "/" + golden_mtl; std::string golden_mtl_str = read_temp_file_in_string(golden_mtl_file_path); - ASSERT_TRUE(strings_equal_after_first_lines(output_mtl_str, golden_mtl_str)); - BLI_delete(out_mtl_file_path.c_str(), false, false); + are_equal = strings_equal_after_first_lines(output_mtl_str, golden_mtl_str); + if (save_failing_test_output && !are_equal) { + 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); + } } } }; @@ -378,6 +416,19 @@ TEST_F(obj_exporter_regression_test, nurbs_as_nurbs) "io_tests/blend_geometry/nurbs.blend", "io_tests/obj/nurbs.obj", "", _export.params); } +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.export_materials = false; + _export.params.export_curves_as_nurbs = true; + compare_obj_export_to_golden("io_tests/blend_geometry/nurbs_curves.blend", + "io_tests/obj/nurbs_curves.obj", + "", + _export.params); +} + TEST_F(obj_exporter_regression_test, nurbs_as_mesh) { OBJExportParamsDefault _export; @@ -402,6 +453,18 @@ TEST_F(obj_exporter_regression_test, cube_all_data_triangulated) _export.params); } +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.export_materials = false; + compare_obj_export_to_golden("io_tests/blend_geometry/cube_normal_edit.blend", + "io_tests/obj/cube_normal_edit.obj", + "", + _export.params); +} + TEST_F(obj_exporter_regression_test, suzanne_all_data) { OBJExportParamsDefault _export; |