diff options
-rw-r--r-- | source/blender/blenkernel/BKE_customdata.h | 6 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/customdata.cc | 7 | ||||
-rw-r--r-- | source/blender/blenlib/BLI_path_util.h | 5 | ||||
-rw-r--r-- | source/blender/blenlib/intern/path_util.c | 18 | ||||
-rw-r--r-- | source/blender/editors/io/io_usd.c | 55 | ||||
-rw-r--r-- | source/blender/io/usd/CMakeLists.txt | 2 | ||||
-rw-r--r-- | source/blender/io/usd/intern/usd_writer_abstract.cc | 39 | ||||
-rw-r--r-- | source/blender/io/usd/intern/usd_writer_abstract.h | 2 | ||||
-rw-r--r-- | source/blender/io/usd/intern/usd_writer_material.cc | 767 | ||||
-rw-r--r-- | source/blender/io/usd/intern/usd_writer_material.h | 55 | ||||
-rw-r--r-- | source/blender/io/usd/intern/usd_writer_mesh.cc | 4 | ||||
-rw-r--r-- | source/blender/io/usd/usd.h | 4 |
12 files changed, 949 insertions, 15 deletions
diff --git a/source/blender/blenkernel/BKE_customdata.h b/source/blender/blenkernel/BKE_customdata.h index 00eae2e8e6e..76389b0c66f 100644 --- a/source/blender/blenkernel/BKE_customdata.h +++ b/source/blender/blenkernel/BKE_customdata.h @@ -437,6 +437,12 @@ int CustomData_get_clone_layer(const struct CustomData *data, int type); int CustomData_get_stencil_layer(const struct CustomData *data, int type); /** + * Returns name of the active layer of the given type or NULL + * if no such active layer is defined. + */ +const char *CustomData_get_active_layer_name(const struct CustomData *data, int type); + +/** * Copies the data from source to the data element at index in the first layer of type * no effect if there is no layer of type. */ diff --git a/source/blender/blenkernel/intern/customdata.cc b/source/blender/blenkernel/intern/customdata.cc index 5e3beab9b72..02b50027ef9 100644 --- a/source/blender/blenkernel/intern/customdata.cc +++ b/source/blender/blenkernel/intern/customdata.cc @@ -2427,6 +2427,13 @@ int CustomData_get_stencil_layer(const CustomData *data, int type) return (layer_index != -1) ? data->layers[layer_index].active_mask : -1; } +const char *CustomData_get_active_layer_name(const struct CustomData *data, const int type) +{ + /* Get the layer index of the active layer of this type. */ + const int layer_index = CustomData_get_active_layer_index(data, type); + return layer_index < 0 ? NULL : data->layers[layer_index].name; +} + void CustomData_set_layer_active(CustomData *data, int type, int n) { for (int i = 0; i < data->totlayer; i++) { diff --git a/source/blender/blenlib/BLI_path_util.h b/source/blender/blenlib/BLI_path_util.h index 658cc0c3825..a2bb8ca47c5 100644 --- a/source/blender/blenlib/BLI_path_util.h +++ b/source/blender/blenlib/BLI_path_util.h @@ -381,6 +381,11 @@ void BLI_path_normalize_unc(char *path_16, int maxlen); #endif /** + * Returns true if the given paths are equal. + */ +bool BLI_paths_equal(const char *p1, const char *p2); + +/** * Appends a suffix to the string, fitting it before the extension * * string = Foo.png, suffix = 123, separator = _ diff --git a/source/blender/blenlib/intern/path_util.c b/source/blender/blenlib/intern/path_util.c index 64bde1193a6..250415c11f9 100644 --- a/source/blender/blenlib/intern/path_util.c +++ b/source/blender/blenlib/intern/path_util.c @@ -1829,3 +1829,21 @@ void BLI_path_slash_native(char *path) BLI_str_replace_char(path + BLI_path_unc_prefix_len(path), ALTSEP, SEP); #endif } + +bool BLI_paths_equal(const char *p1, const char *p2) +{ + /* Normalize the paths so we can compare them. */ + char norm_p1[FILE_MAX]; + char norm_p2[FILE_MAX]; + + BLI_strncpy(norm_p1, p1, sizeof(norm_p1)); + BLI_strncpy(norm_p2, p2, sizeof(norm_p2)); + + BLI_path_slash_native(norm_p1); + BLI_path_slash_native(norm_p2); + + BLI_path_normalize(NULL, norm_p1); + BLI_path_normalize(NULL, norm_p2); + + return BLI_path_cmp(norm_p1, norm_p2) == 0; +} diff --git a/source/blender/editors/io/io_usd.c b/source/blender/editors/io/io_usd.c index 4e2ccea36ab..49d60ede277 100644 --- a/source/blender/editors/io/io_usd.c +++ b/source/blender/editors/io/io_usd.c @@ -131,6 +131,11 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op) const bool use_instancing = RNA_boolean_get(op->ptr, "use_instancing"); const bool evaluation_mode = RNA_enum_get(op->ptr, "evaluation_mode"); + const bool generate_preview_surface = RNA_boolean_get(op->ptr, "generate_preview_surface"); + const bool export_textures = RNA_boolean_get(op->ptr, "export_textures"); + const bool overwrite_textures = RNA_boolean_get(op->ptr, "overwrite_textures"); + const bool relative_texture_paths = RNA_boolean_get(op->ptr, "relative_texture_paths"); + struct USDExportParams params = { export_animation, export_hair, @@ -141,6 +146,10 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op) visible_objects_only, use_instancing, evaluation_mode, + generate_preview_surface, + export_textures, + overwrite_textures, + relative_texture_paths, }; bool ok = USD_export(C, filename, ¶ms, as_background_job); @@ -173,6 +182,26 @@ static void wm_usd_export_draw(bContext *UNUSED(C), wmOperator *op) uiItemR(col, ptr, "evaluation_mode", 0, NULL, ICON_NONE); box = uiLayoutBox(layout); + col = uiLayoutColumnWithHeading(box, true, IFACE_("Materials")); + uiItemR(col, ptr, "generate_preview_surface", 0, NULL, ICON_NONE); + const bool export_mtl = RNA_boolean_get(ptr, "export_materials"); + uiLayoutSetActive(col, export_mtl); + + uiLayout *row = uiLayoutRow(col, true); + uiItemR(row, ptr, "export_textures", 0, NULL, ICON_NONE); + const bool preview = RNA_boolean_get(ptr, "generate_preview_surface"); + uiLayoutSetActive(row, export_mtl && preview); + + row = uiLayoutRow(col, true); + uiItemR(row, ptr, "overwrite_textures", 0, NULL, ICON_NONE); + const bool export_tex = RNA_boolean_get(ptr, "export_textures"); + uiLayoutSetActive(row, export_mtl && preview && export_tex); + + row = uiLayoutRow(col, true); + uiItemR(row, ptr, "relative_texture_paths", 0, NULL, ICON_NONE); + uiLayoutSetActive(row, export_mtl && preview); + + box = uiLayoutBox(layout); uiItemL(box, IFACE_("Experimental"), ICON_NONE); uiItemR(box, ptr, "use_instancing", 0, NULL, ICON_NONE); } @@ -249,6 +278,32 @@ void WM_OT_usd_export(struct wmOperatorType *ot) "Use Settings for", "Determines visibility of objects, modifier settings, and other areas where there " "are different settings for viewport and rendering"); + + RNA_def_boolean(ot->srna, + "generate_preview_surface", + true, + "To USD Preview Surface", + "Generate an approximate USD Preview Surface shader " + "representation of a Principled BSDF node network"); + + RNA_def_boolean(ot->srna, + "export_textures", + true, + "Export Textures", + "If exporting materials, export textures referenced by material nodes " + "to a 'textures' directory in the same directory as the USD file"); + + RNA_def_boolean(ot->srna, + "overwrite_textures", + false, + "Overwrite Textures", + "Allow overwriting existing texture files when exporting textures"); + + RNA_def_boolean(ot->srna, + "relative_texture_paths", + true, + "Relative Texture Paths", + "Make texture asset paths relative to the USD file"); } /* ====== USD Import ====== */ 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_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..34fd884f51a --- /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 == NULL) { + 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, NULL); + + if (!allow_overwrite && BLI_exists(export_path)) { + return; + } + + if (BLI_paths_equal(export_path, image_abs_path) && 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 occurrance + * 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 occurence 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, NULL); + } + 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, NULL); + } + 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_paths_equal(src_tile_path, dest_tile_path)) { + /* 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, NULL); + + if (!allow_overwrite && BLI_exists(dest_path)) { + return; + } + + if (BLI_paths_equal(source_path, dest_path)) { + /* 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, NULL); + + 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..b2c732963dc --- /dev/null +++ b/source/blender/io/usd/intern/usd_writer_material.h @@ -0,0 +1,55 @@ +/* + * 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. + * + * The 'default_uv' paramter is 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 { |