From e1a6749b3d98fe0fcb2e23d25e08849bc77c77cb Mon Sep 17 00:00:00 2001 From: Charles Wardlaw Date: Thu, 30 May 2024 20:48:43 +0200 Subject: [PATCH] USD: dome light IO This commit adds logic to convert between USD dome lights and Blender world materials. The USD dome light rotation is represented in a mapping node input to the environment texture. If the dome light has a color specified in addition to the texture map, the color will be converted to a vector multiply on the the environment texture output. I the imported USD has multiple dome lights, only the first dome light will be converted to a world material. Co-authored-by: kiki Co-authored-by: Michael Kowalski Pull Request: https://projects.blender.org/blender/blender/pulls/121800 --- source/blender/editors/io/io_usd.cc | 26 +- source/blender/io/usd/CMakeLists.txt | 4 + .../blender/io/usd/intern/usd_asset_utils.cc | 17 +- .../blender/io/usd/intern/usd_asset_utils.hh | 6 + .../blender/io/usd/intern/usd_capi_export.cc | 8 + .../blender/io/usd/intern/usd_capi_import.cc | 8 + .../io/usd/intern/usd_light_convert.cc | 562 ++++++++++++++++++ .../io/usd/intern/usd_light_convert.hh | 31 + .../io/usd/intern/usd_reader_material.cc | 17 - .../blender/io/usd/intern/usd_reader_stage.cc | 13 + .../blender/io/usd/intern/usd_reader_stage.hh | 10 + .../io/usd/intern/usd_writer_material.cc | 64 +- .../io/usd/intern/usd_writer_material.hh | 20 + source/blender/io/usd/usd.hh | 2 + tests/python/bl_usd_export_test.py | 1 + 15 files changed, 737 insertions(+), 52 deletions(-) create mode 100644 source/blender/io/usd/intern/usd_light_convert.cc create mode 100644 source/blender/io/usd/intern/usd_light_convert.hh diff --git a/source/blender/editors/io/io_usd.cc b/source/blender/editors/io/io_usd.cc index eee6a4334b3..a42df2026db 100644 --- a/source/blender/editors/io/io_usd.cc +++ b/source/blender/editors/io/io_usd.cc @@ -242,6 +242,8 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op) const int global_forward = RNA_enum_get(op->ptr, "export_global_forward_selection"); const int global_up = RNA_enum_get(op->ptr, "export_global_up_selection"); + const bool convert_world_material = RNA_boolean_get(op->ptr, "convert_world_material"); + const eUSDXformOpMode xform_op_mode = eUSDXformOpMode(RNA_enum_get(op->ptr, "xform_op_mode")); char root_prim_path[FILE_MAX]; @@ -275,6 +277,7 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op) convert_orientation, eIOAxis(global_forward), eIOAxis(global_up), + convert_world_material, xform_op_mode, export_meshes, export_lights, @@ -320,6 +323,8 @@ static void wm_usd_export_draw(bContext *C, wmOperator *op) uiItemR(row, ptr, "author_blender_name", UI_ITEM_NONE, nullptr, ICON_NONE); uiLayoutSetActive(row, RNA_boolean_get(op->ptr, "export_custom_properties")); + uiItemR(col, ptr, "convert_world_material", UI_ITEM_NONE, nullptr, ICON_NONE); + col = uiLayoutColumn(box, true); uiItemR(col, ptr, "triangulate_meshes", UI_ITEM_NONE, nullptr, ICON_NONE); @@ -608,6 +613,15 @@ void WM_OT_usd_export(wmOperatorType *ot) "Author USD custom attributes containing the original Blender object and " "object data names"); + RNA_def_boolean( + ot->srna, + "convert_world_material", + true, + "Convert World Material", + "Convert the world material to a USD dome light. " + "Currently works for simple materials, consisting of an environment texture " + "connected to a background shader, with an optional vector multiply of the texture color"); + RNA_def_boolean(ot->srna, "export_meshes", true, "Meshes", "Export all meshes"); RNA_def_boolean(ot->srna, "export_lights", true, "Lights", "Export all lights"); @@ -725,6 +739,8 @@ static int wm_usd_import_exec(bContext *C, wmOperator *op) const bool validate_meshes = RNA_boolean_get(op->ptr, "validate_meshes"); + const bool create_world_material = RNA_boolean_get(op->ptr, "create_world_material"); + /* TODO(makowalski): Add support for sequences. */ const bool is_sequence = false; int offset = 0; @@ -783,6 +799,7 @@ static int wm_usd_import_exec(bContext *C, wmOperator *op) params.tex_name_collision_mode = tex_name_collision_mode; params.import_all_materials = import_all_materials; params.attr_import_mode = attr_import_mode; + params.create_world_material = create_world_material; STRNCPY(params.import_textures_dir, import_textures_dir); @@ -837,7 +854,8 @@ static void wm_usd_import_draw(bContext * /*C*/, wmOperator *op) uiItemR(col, ptr, "set_frame_range", UI_ITEM_NONE, nullptr, ICON_NONE); uiItemR(col, ptr, "relative_path", UI_ITEM_NONE, nullptr, ICON_NONE); uiItemR(col, ptr, "create_collection", UI_ITEM_NONE, nullptr, ICON_NONE); - uiItemR(box, ptr, "light_intensity_scale", UI_ITEM_NONE, nullptr, ICON_NONE); + uiItemR(col, ptr, "light_intensity_scale", UI_ITEM_NONE, nullptr, ICON_NONE); + uiItemR(col, ptr, "create_world_material", UI_ITEM_NONE, nullptr, ICON_NONE); uiItemR(col, ptr, "attr_import_mode", UI_ITEM_NONE, nullptr, ICON_NONE); box = uiLayoutBox(layout); @@ -1045,6 +1063,12 @@ void WM_OT_usd_import(wmOperatorType *ot) "Ensure the data is valid " "(when disabled, data may be imported which causes crashes displaying or editing)"); + RNA_def_boolean(ot->srna, + "create_world_material", + true, + "Create World Material", + "Convert the first discovered USD dome light to a world background shader"); + RNA_def_boolean(ot->srna, "import_defined_only", true, diff --git a/source/blender/io/usd/CMakeLists.txt b/source/blender/io/usd/CMakeLists.txt index 04617e9849b..c85dcd83ab1 100644 --- a/source/blender/io/usd/CMakeLists.txt +++ b/source/blender/io/usd/CMakeLists.txt @@ -95,7 +95,9 @@ set(SRC intern/usd_capi_import.cc intern/usd_hierarchy_iterator.cc intern/usd_hook.cc + intern/usd_light_convert.cc intern/usd_mesh_utils.cc + intern/usd_writer_abstract.cc intern/usd_writer_armature.cc intern/usd_writer_camera.cc @@ -139,7 +141,9 @@ set(SRC intern/usd_hash_types.hh intern/usd_hierarchy_iterator.hh intern/usd_hook.hh + intern/usd_light_convert.hh intern/usd_mesh_utils.hh + intern/usd_writer_abstract.hh intern/usd_writer_armature.hh intern/usd_writer_camera.hh diff --git a/source/blender/io/usd/intern/usd_asset_utils.cc b/source/blender/io/usd/intern/usd_asset_utils.cc index 28f9b30219c..ea475583ca5 100644 --- a/source/blender/io/usd/intern/usd_asset_utils.cc +++ b/source/blender/io/usd/intern/usd_asset_utils.cc @@ -10,10 +10,11 @@ #include #include +#include "BKE_appdir.hh" #include "BKE_main.hh" #include "BKE_report.hh" -#include "BLI_fileops.h" +#include "BLI_fileops.hh" #include "BLI_path_util.h" #include "BLI_string.h" @@ -330,4 +331,18 @@ bool is_udim_path(const std::string &path) path.find(UDIM_PATTERN2) != std::string::npos; } +const char *temp_textures_dir() +{ + static bool inited = false; + + static char temp_dir[FILE_MAXDIR] = {'\0'}; + + if (!inited) { + BLI_path_join(temp_dir, sizeof(temp_dir), BKE_tempdir_session(), "usd_textures_tmp", SEP_STR); + inited = true; + } + + return temp_dir; +} + } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_asset_utils.hh b/source/blender/io/usd/intern/usd_asset_utils.hh index 82fc448f140..ba79c77a8d8 100644 --- a/source/blender/io/usd/intern/usd_asset_utils.hh +++ b/source/blender/io/usd/intern/usd_asset_utils.hh @@ -61,4 +61,10 @@ std::string import_asset(const char *src, */ bool is_udim_path(const std::string &path); +/** + * Returns path to temporary folder for saving imported textures prior to packing. + * CAUTION: this directory is recursively deleted after material import. + */ +const char *temp_textures_dir(); + } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_capi_export.cc b/source/blender/io/usd/intern/usd_capi_export.cc index a5387eb5aa1..0cbce032379 100644 --- a/source/blender/io/usd/intern/usd_capi_export.cc +++ b/source/blender/io/usd/intern/usd_capi_export.cc @@ -8,6 +8,7 @@ #include "usd.hh" #include "usd_hierarchy_iterator.hh" #include "usd_hook.hh" +#include "usd_light_convert.hh" #include "usd_private.hh" #include @@ -342,6 +343,13 @@ pxr::UsdStageRefPtr export_to_stage(const USDExportParams ¶ms, iter.process_usd_skel(); } + /* Creating dome lights should be called after writers have + * completed, to avoid a name collision when creating the light + * prim. */ + if (!params.selected_objects_only && params.convert_world_material) { + world_material_to_dome_light(params, scene, usd_stage); + } + /* Set the default prim if it doesn't exist */ if (!usd_stage->GetDefaultPrim()) { /* Use TraverseAll since it's guaranteed to be depth first and will get the first top level diff --git a/source/blender/io/usd/intern/usd_capi_import.cc b/source/blender/io/usd/intern/usd_capi_import.cc index c8c8b5a9f91..16920bfe88b 100644 --- a/source/blender/io/usd/intern/usd_capi_import.cc +++ b/source/blender/io/usd/intern/usd_capi_import.cc @@ -5,6 +5,7 @@ #include "IO_types.hh" #include "usd.hh" #include "usd_hook.hh" +#include "usd_light_convert.hh" #include "usd_reader_geom.hh" #include "usd_reader_prim.hh" #include "usd_reader_stage.hh" @@ -297,6 +298,13 @@ static void import_startjob(void *customdata, wmJobWorkerStatus *worker_status) archive->collect_readers(); + if (data->params.import_lights && data->params.create_world_material && + !archive->dome_lights().is_empty()) + { + dome_light_to_world_material( + data->params, data->settings, data->scene, data->bmain, archive->dome_lights().first()); + } + if (data->params.import_materials && data->params.import_all_materials) { archive->import_all_materials(data->bmain); } diff --git a/source/blender/io/usd/intern/usd_light_convert.cc b/source/blender/io/usd/intern/usd_light_convert.cc new file mode 100644 index 00000000000..f49437957b2 --- /dev/null +++ b/source/blender/io/usd/intern/usd_light_convert.cc @@ -0,0 +1,562 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "usd_light_convert.hh" + +#include "usd.hh" +#include "usd_asset_utils.hh" +#include "usd_reader_prim.hh" +#include "usd_writer_material.hh" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "BKE_image.h" +#include "BKE_light.h" +#include "BKE_main.hh" +#include "BKE_node.hh" +#include "BKE_node_runtime.hh" +#include "BKE_node_tree_update.hh" +#include "BKE_scene.hh" +#include "BLI_fileops.h" +#include "BLI_listbase.h" +#include "BLI_math_rotation.h" +#include "BLI_math_vector.h" +#include "BLI_path_util.h" +#include "BLI_string.h" +#include "DNA_light_types.h" +#include "DNA_node_types.h" +#include "DNA_scene_types.h" +#include "DNA_world_types.h" +#include "ED_node.hh" + +#include + +#include "CLG_log.h" +static CLG_LogRef LOG = {"io.usd"}; + +namespace usdtokens { +// Attribute names. +static const pxr::TfToken color("color", pxr::TfToken::Immortal); +static const pxr::TfToken intensity("intensity", pxr::TfToken::Immortal); +static const pxr::TfToken texture_file("texture:file", pxr::TfToken::Immortal); +} // namespace usdtokens + +namespace { + +/* If the given attribute has an authored value, return its value in the r_value + * out parameter. + * + * We wish to support older UsdLux APIs in older versions of USD. For example, + * in previous versions of the API, shader input attibutes did not have the + * "inputs:" prefix. One can provide the older input attibute name in the + * 'fallback_attr_name' argument, and that attribute will be queried if 'attr' + * doesn't exist or doesn't have an authored value. + */ +template +bool get_authored_value(const pxr::UsdAttribute attr, + const double motionSampleTime, + T *r_value, + const pxr::UsdPrim prim = pxr::UsdPrim(), + const pxr::TfToken fallback_attr_name = pxr::TfToken()) +{ + if (attr && attr.HasAuthoredValue()) { + return attr.Get(r_value, motionSampleTime); + } + + if (!prim || fallback_attr_name.IsEmpty()) { + return false; + } + + pxr::UsdAttribute fallback_attr = prim.GetAttribute(fallback_attr_name); + if (fallback_attr && fallback_attr.HasAuthoredValue()) { + return fallback_attr.Get(r_value, motionSampleTime); + } + + return false; +} + +/* Helper struct for retrieving shader information when traversing a world material + * node chain, provided as user data for bke::nodeChainIter(). */ +struct WorldNtreeSearchResults { + const blender::io::usd::USDExportParams params; + pxr::UsdStageRefPtr stage; + + float world_color[3]; + float world_intensity; + float mapping_rot[3]; + + std::string file_path; + + float color_mult[3]; + + bool background_found; + bool env_tex_found; + bool mult_found; + bool mapping_found; + + WorldNtreeSearchResults(const blender::io::usd::USDExportParams &in_params, + pxr::UsdStageRefPtr in_stage) + : params(in_params), + stage(in_stage), + world_intensity(0.0f), + background_found(false), + env_tex_found(false), + mult_found(false) + { + } +}; + +} // End anonymous namespace. + +namespace blender::io::usd { + +/* If the given path already exists on the given stage, return the path with + * a numerical suffix appende to the name that ensures the path is unique. If + * the path does not exist on the stage, it will be returned unchanged. */ +static pxr::SdfPath get_unique_path(pxr::UsdStageRefPtr stage, const std::string &path) +{ + std::string unique_path = path; + int suffix = 2; + while (stage->GetPrimAtPath(pxr::SdfPath(unique_path)).IsValid()) { + unique_path = path + std::to_string(suffix++); + } + return pxr::SdfPath(unique_path); +} + +/* Load the image at the given path. Handle packing and copying based in the import options. + * Return the opened image on succsss or a nullptr on failure. */ +static Image *load_image(std::string tex_path, Main *bmain, const USDImportParams ¶ms) +{ + /* Optionally copy the asset if it's inside a USDZ package. */ + const bool import_textures = params.import_textures_mode != USD_TEX_IMPORT_NONE && + pxr::ArIsPackageRelativePath(tex_path); + + if (import_textures) { + /* If we are packing the imported textures, we first write them + * to a temporary directory. */ + const char *textures_dir = params.import_textures_mode == USD_TEX_IMPORT_PACK ? + temp_textures_dir() : + params.import_textures_dir; + + const eUSDTexNameCollisionMode name_collision_mode = params.import_textures_mode == + USD_TEX_IMPORT_PACK ? + USD_TEX_NAME_COLLISION_OVERWRITE : + params.tex_name_collision_mode; + + tex_path = import_asset(tex_path.c_str(), textures_dir, name_collision_mode, nullptr); + } + + Image *image = BKE_image_load_exists(bmain, tex_path.c_str()); + if (!image) { + return nullptr; + } + + if (import_textures && params.import_textures_mode == USD_TEX_IMPORT_PACK && + !BKE_image_has_packedfile(image)) + { + BKE_image_packfiles(nullptr, image, ID_BLEND_PATH(bmain, &image->id)); + if (BLI_is_dir(temp_textures_dir())) { + BLI_delete(temp_textures_dir(), true, true); + } + } + + return image; +} + +/* Create a new node of type 'new_node_type' and connect it + * as an upstream source to 'dst_node' with the given sockets. */ +static bNode *append_node(bNode *dst_node, + int16_t new_node_type, + const char *out_sock, + const char *in_sock, + bNodeTree *ntree, + float offset) +{ + bNode *src_node = bke::nodeAddStaticNode(NULL, ntree, new_node_type); + + if (!src_node) { + return nullptr; + } + + bke::nodeAddLink(ntree, + src_node, + bke::nodeFindSocket(src_node, SOCK_OUT, out_sock), + dst_node, + bke::nodeFindSocket(dst_node, SOCK_IN, in_sock)); + + src_node->locx = dst_node->locx - offset; + src_node->locy = dst_node->locy; + + return src_node; +} + +/* Callback function for iterating over a shader node chain to retrieve data + * necessary for converting a world material to a USD dome light. It also + * handles copying textures, if required. */ +static bool node_search(bNode *fromnode, bNode * /* tonode */, void *userdata, const bool reversed) +{ + if (!(userdata && fromnode)) { + return true; + } + + WorldNtreeSearchResults *res = reinterpret_cast(userdata); + + if (!res->background_found && fromnode->type == SH_NODE_BACKGROUND) { + /* Get light color and intensity */ + bNodeSocketValueRGBA *color_data = bke::nodeFindSocket(fromnode, SOCK_IN, "Color") + ->default_value_typed(); + bNodeSocketValueFloat *strength_data = bke::nodeFindSocket(fromnode, SOCK_IN, "Strength") + ->default_value_typed(); + + res->background_found = true; + res->world_intensity = strength_data->value; + res->world_color[0] = color_data->value[0]; + res->world_color[1] = color_data->value[1]; + res->world_color[2] = color_data->value[2]; + } + else if (!res->env_tex_found && fromnode->type == SH_NODE_TEX_ENVIRONMENT) { + /* Get env tex path. */ + + res->file_path = get_tex_image_asset_filepath(fromnode, res->stage, res->params); + + if (!res->file_path.empty()) { + res->env_tex_found = true; + if (res->params.export_textures) { + export_texture(fromnode, res->stage, res->params.overwrite_textures); + } + } + } + else if (!res->env_tex_found && !res->mult_found && fromnode->type == SH_NODE_VECTOR_MATH) { + + if (fromnode->custom1 == NODE_VECTOR_MATH_MULTIPLY) { + res->mult_found = true; + + bNodeSocket *vec_sock = bke::nodeFindSocket(fromnode, SOCK_IN, "Vector"); + if (vec_sock) { + vec_sock = vec_sock->next; + } + + if (vec_sock) { + copy_v3_v3(res->color_mult, ((bNodeSocketValueVector *)vec_sock->default_value)->value); + } + } + } + else if (res->env_tex_found && fromnode->type == SH_NODE_MAPPING) { + res->mapping_found = true; + copy_v3_fl(res->mapping_rot, 0.0f); + if (bNodeSocket *socket = bke::nodeFindSocket(fromnode, SOCK_IN, "Rotation")) { + bNodeSocketValueVector *rot_value = static_cast( + socket->default_value); + copy_v3_v3(res->mapping_rot, rot_value->value); + } + } + + return true; +} + +/* If the Blender scene has an environment texture, + * export it as a USD dome light. */ +void world_material_to_dome_light(const USDExportParams ¶ms, + const Scene *scene, + pxr::UsdStageRefPtr stage) +{ + if (!(stage && scene && scene->world && scene->world->use_nodes && scene->world->nodetree)) { + return; + } + + /* Find the world output. */ + const bNodeTree *ntree = scene->world->nodetree; + ntree->ensure_topology_cache(); + const blender::Span bsdf_nodes = ntree->nodes_by_type("ShaderNodeOutputWorld"); + const bNode *output = bsdf_nodes.is_empty() ? nullptr : bsdf_nodes.first(); + + if (!output) { + /* No output, no valid network to convert. */ + return; + } + + WorldNtreeSearchResults res(params, stage); + + bke::nodeChainIter(scene->world->nodetree, output, node_search, &res, true); + + if (!(res.background_found || res.env_tex_found)) { + /* No nodes to convert */ + return; + } + + /* Create USD dome light. */ + + pxr::SdfPath env_light_path = get_unique_path(stage, + std::string(params.root_prim_path) + "/env_light"); + + pxr::UsdLuxDomeLight dome_light = pxr::UsdLuxDomeLight::Define(stage, env_light_path); + + if (res.env_tex_found) { + pxr::SdfAssetPath path(res.file_path); + dome_light.CreateTextureFileAttr().Set(path); + + if (res.mult_found) { + pxr::GfVec3f color_val(res.color_mult[0], res.color_mult[1], res.color_mult[2]); + dome_light.CreateColorAttr().Set(color_val); + } + } + else { + pxr::GfVec3f color_val(res.world_color[0], res.world_color[1], res.world_color[2]); + dome_light.CreateColorAttr().Set(color_val); + } + + if (res.background_found) { + dome_light.CreateIntensityAttr().Set(res.world_intensity); + } + + /* We always set a default rotation on the light, whether or not res.mapping_found + * is true, since res.mapping_rot defaults to zeros. */ + + /* Convert radians to degrees. */ + mul_v3_fl(res.mapping_rot, 180.0f / M_PI); + + pxr::GfMatrix4d xf = + pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(1.0, 0.0, 0.0), 90.0)) * + pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(0.0, 0.0, 1.0), 90.0)) * + pxr::GfMatrix4d().SetRotate( + pxr::GfRotation(pxr::GfVec3d(0.0, 0.0, 1.0), -res.mapping_rot[2])) * + pxr::GfMatrix4d().SetRotate( + pxr::GfRotation(pxr::GfVec3d(0.0, 1.0, 0.0), -res.mapping_rot[1])) * + pxr::GfMatrix4d().SetRotate( + pxr::GfRotation(pxr::GfVec3d(1.0, 0.0, 0.0), -res.mapping_rot[0])); + + pxr::GfVec3d angles = xf.DecomposeRotation( + pxr::GfVec3d::ZAxis(), pxr::GfVec3d::YAxis(), pxr::GfVec3d::XAxis()); + + pxr::GfVec3f rot_vec(angles[2], angles[1], angles[0]); + + pxr::UsdGeomXformCommonAPI xform_api(dome_light); + xform_api.SetRotate(rot_vec, pxr::UsdGeomXformCommonAPI::RotationOrderXYZ); +} + +/* Import the dome light as a world material. */ + +void dome_light_to_world_material(const USDImportParams ¶ms, + const ImportSettings &settings, + Scene *scene, + Main *bmain, + const pxr::UsdLuxDomeLight &dome_light, + const double time) +{ + if (!(scene && scene->world && dome_light)) { + return; + } + + if (!scene->world->use_nodes) { + scene->world->use_nodes = true; + } + + if (!scene->world->nodetree) { + scene->world->nodetree = bke::ntreeAddTree(NULL, "Shader Nodetree", "ShaderNodeTree"); + if (!scene->world->nodetree) { + CLOG_WARN(&LOG, "Couldn't create world ntree"); + return; + } + } + + bNodeTree *ntree = scene->world->nodetree; + bNode *output = nullptr; + bNode *bgshader = nullptr; + + /* We never delete existing nodes, but we might disconnect them + * and move them out of the way. */ + + /* Look for the output and background shader nodes, which we will reuse. */ + LISTBASE_FOREACH (bNode *, node, &ntree->nodes) { + if (node->type == SH_NODE_OUTPUT_WORLD) { + output = node; + } + else if (node->type == SH_NODE_BACKGROUND) { + bgshader = node; + } + else { + /* Move existing node out of the way. */ + node->locy += 300; + } + } + + /* Create the output and background shader nodes, if they don't exist. */ + if (!output) { + output = bke::nodeAddStaticNode(NULL, ntree, SH_NODE_OUTPUT_WORLD); + + if (!output) { + CLOG_WARN(&LOG, "Couldn't create world output node"); + return; + } + + output->locx = 300.0f; + output->locy = 300.0f; + } + + if (!bgshader) { + bgshader = append_node(output, SH_NODE_BACKGROUND, "Background", "Surface", ntree, 200); + + if (!bgshader) { + CLOG_WARN(&LOG, "Couldn't create world shader node"); + return; + } + + /* Set the default background color. */ + bNodeSocket *color_sock = bke::nodeFindSocket(bgshader, SOCK_IN, "Color"); + copy_v3_v3(((bNodeSocketValueRGBA *)color_sock->default_value)->value, &scene->world->horr); + } + + /* Make sure the first input to the shader node is disconnected. */ + bNodeSocket *shader_input = bke::nodeFindSocket(bgshader, SOCK_IN, "Color"); + + if (shader_input && shader_input->link) { + bke::nodeRemLink(ntree, shader_input->link); + } + + /* Set the background shader intensity. */ + float intensity = 1.0f; + get_authored_value( + dome_light.GetIntensityAttr(), time, &intensity, dome_light.GetPrim(), usdtokens::intensity); + + intensity *= params.light_intensity_scale; + + bNodeSocket *strength_sock = bke::nodeFindSocket(bgshader, SOCK_IN, "Strength"); + ((bNodeSocketValueFloat *)strength_sock->default_value)->value = intensity; + + /* Get the dome light texture file and color. */ + pxr::SdfAssetPath tex_path; + bool has_tex = get_authored_value(dome_light.GetTextureFileAttr(), + time, + &tex_path, + dome_light.GetPrim(), + usdtokens::texture_file); + + pxr::GfVec3f color; + bool has_color = get_authored_value( + dome_light.GetColorAttr(), time, &color, dome_light.GetPrim(), usdtokens::color); + + if (!has_tex) { + /* No texture file is authored on the dome light. Set the color, if it was authored, + * and return early. */ + if (has_color) { + bNodeSocket *color_sock = bke::nodeFindSocket(bgshader, SOCK_IN, "Color"); + copy_v3_v3(((bNodeSocketValueRGBA *)color_sock->default_value)->value, color.data()); + } + + bke::nodeSetActive(ntree, output); + BKE_ntree_update_main_tree(bmain, ntree, nullptr); + + return; + } + + /* If the light has authored color, create a color multiply node for the environment + * texture output. */ + bNode *mult = nullptr; + + if (has_color) { + mult = append_node(bgshader, SH_NODE_VECTOR_MATH, "Vector", "Color", ntree, 200); + + if (!mult) { + CLOG_WARN(&LOG, "Couldn't create vector multiply node"); + return; + } + + mult->custom1 = NODE_VECTOR_MATH_MULTIPLY; + + /* Set the color in the vector math node's second socket. */ + bNodeSocket *vec_sock = bke::nodeFindSocket(mult, SOCK_IN, "Vector"); + if (vec_sock) { + vec_sock = vec_sock->next; + } + + if (vec_sock) { + copy_v3_v3(((bNodeSocketValueVector *)vec_sock->default_value)->value, color.data()); + } + else { + CLOG_WARN(&LOG, "Couldn't find vector multiply second vector socket"); + } + } + + bNode *tex = nullptr; + + /* Append an environment texture node to the mult node, if it was created, or directly to + * the background shader. */ + if (mult) { + tex = append_node(mult, SH_NODE_TEX_ENVIRONMENT, "Color", "Vector", ntree, 400); + } + else { + tex = append_node(bgshader, SH_NODE_TEX_ENVIRONMENT, "Color", "Color", ntree, 400); + } + + if (!tex) { + CLOG_WARN(&LOG, "Couldn't create world environment texture node"); + return; + } + + bNode *mapping = append_node(tex, SH_NODE_MAPPING, "Vector", "Vector", ntree, 200); + + if (!mapping) { + CLOG_WARN(&LOG, "Couldn't create mapping node"); + return; + } + + bNode *tex_coord = append_node(mapping, SH_NODE_TEX_COORD, "Generated", "Vector", ntree, 200); + + if (!tex_coord) { + CLOG_WARN(&LOG, "Couldn't create texture coordinate node"); + return; + } + + /* Load the texture image. */ + std::string resolved_path = tex_path.GetResolvedPath(); + + if (resolved_path.empty()) { + CLOG_WARN(&LOG, "Couldn't get resolved path for asset %s", tex_path.GetAssetPath().c_str()); + return; + } + + Image *image = load_image(resolved_path, bmain, params); + if (!image) { + CLOG_WARN(&LOG, "Couldn't load image file %s", resolved_path.c_str()); + return; + } + + tex->id = &image->id; + + /* Set the transform. */ + pxr::UsdGeomXformCache xf_cache(time); + pxr::GfMatrix4d xf = xf_cache.GetLocalToWorldTransform(dome_light.GetPrim()); + + xf = pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(0.0, 0.0, 1.0), -90.0)) * + pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(1.0, 0.0, 0.0), -90.0)) * xf; + + pxr::GfVec3d angles = xf.DecomposeRotation( + pxr::GfVec3d::XAxis(), pxr::GfVec3d::YAxis(), pxr::GfVec3d::ZAxis()); + pxr::GfVec3f rot_vec(-angles[0], -angles[1], -angles[2]); + + /* Convert degrees to radians. */ + rot_vec *= M_PI / 180.0f; + + if (bNodeSocket *socket = bke::nodeFindSocket(mapping, SOCK_IN, "Rotation")) { + bNodeSocketValueVector *rot_value = static_cast( + socket->default_value); + copy_v3_v3(rot_value->value, rot_vec.data()); + } + + bke::nodeSetActive(ntree, output); + DEG_id_tag_update(&ntree->id, ID_RECALC_NTREE_OUTPUT); + BKE_ntree_update_main_tree(bmain, ntree, nullptr); +} + +} // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_light_convert.hh b/source/blender/io/usd/intern/usd_light_convert.hh new file mode 100644 index 00000000000..948adc7888f --- /dev/null +++ b/source/blender/io/usd/intern/usd_light_convert.hh @@ -0,0 +1,31 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include +#include + +struct Light; +struct Main; +struct Scene; + +namespace blender::io::usd { + +struct USDExportParams; +struct USDImportParams; + +struct ImportSettings; + +void world_material_to_dome_light(const USDExportParams ¶ms, + const Scene *scene, + pxr::UsdStageRefPtr stage); + +void dome_light_to_world_material(const USDImportParams ¶ms, + const ImportSettings &settings, + Scene *scene, + Main *bmain, + const pxr::UsdLuxDomeLight &dome_light, + const double time = 0.0); + +} // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_reader_material.cc b/source/blender/io/usd/intern/usd_reader_material.cc index 60d550df917..3c6a3e23d91 100644 --- a/source/blender/io/usd/intern/usd_reader_material.cc +++ b/source/blender/io/usd/intern/usd_reader_material.cc @@ -89,23 +89,6 @@ static const pxr::TfToken UsdUVTexture("UsdUVTexture", pxr::TfToken::Immortal); static const pxr::TfToken UsdTransform2d("UsdTransform2d", pxr::TfToken::Immortal); } // namespace usdtokens -/* Temporary folder for saving imported textures prior to packing. - * CAUTION: this directory is recursively deleted after material - * import. */ -static const char *temp_textures_dir() -{ - static bool inited = false; - - static char temp_dir[FILE_MAXDIR] = {'\0'}; - - if (!inited) { - BLI_path_join(temp_dir, sizeof(temp_dir), BKE_tempdir_session(), "usd_textures_tmp", SEP_STR); - inited = true; - } - - return temp_dir; -} - using blender::io::usd::ShaderToNodeMap; /** diff --git a/source/blender/io/usd/intern/usd_reader_stage.cc b/source/blender/io/usd/intern/usd_reader_stage.cc index 3e46f117a41..1583ced7167 100644 --- a/source/blender/io/usd/intern/usd_reader_stage.cc +++ b/source/blender/io/usd/intern/usd_reader_stage.cc @@ -181,6 +181,10 @@ USDPrimReader *USDStageReader::create_reader_if_allowed(const pxr::UsdPrim &prim if (params_.import_meshes && prim.IsA()) { return new USDMeshReader(prim, params_, settings_); } + if (params_.import_lights && prim.IsA()) { + /* Dome lights are handled elsewhere. */ + return nullptr; + } #if PXR_VERSION >= 2111 if (params_.import_lights && (prim.IsA() || prim.IsA())) @@ -226,6 +230,10 @@ USDPrimReader *USDStageReader::create_reader(const pxr::UsdPrim &prim) if (prim.IsA()) { return new USDMeshReader(prim, params_, settings_); } + if (prim.IsA()) { + /* We don't handle dome lights. */ + return nullptr; + } #if PXR_VERSION >= 2111 if (prim.IsA() || prim.IsA()) { #else @@ -364,6 +372,10 @@ USDPrimReader *USDStageReader::collect_readers(const pxr::UsdPrim &prim, } } + if (prim.IsA()) { + dome_lights_.append(pxr::UsdLuxDomeLight(prim)); + } + pxr::Usd_PrimFlagsConjunction filter_flags = pxr::UsdPrimIsActive && pxr::UsdPrimIsLoaded && !pxr::UsdPrimIsAbstract; @@ -446,6 +458,7 @@ void USDStageReader::collect_readers() } clear_readers(); + dome_lights_.clear(); /* Identify paths to point instancer prototypes, as these will be converted * in a separate pass over the stage. */ diff --git a/source/blender/io/usd/intern/usd_reader_stage.hh b/source/blender/io/usd/intern/usd_reader_stage.hh index 079d5bd6a5c..8323789c807 100644 --- a/source/blender/io/usd/intern/usd_reader_stage.hh +++ b/source/blender/io/usd/intern/usd_reader_stage.hh @@ -12,6 +12,7 @@ #include "usd_reader_prim.hh" #include +#include #include @@ -41,6 +42,10 @@ class USDStageReader { blender::Vector readers_; + /* USD dome lights are converted to a world material, + * rather than light objects, so are handled differently */ + blender::Vector dome_lights_; + /* USD material prim paths encountered during stage * traversal, for importing unused materials. */ blender::Vector material_paths_; @@ -112,6 +117,11 @@ class USDStageReader { return readers_; }; + const blender::Vector &dome_lights() const + { + return dome_lights_; + }; + void sort_readers(); /** diff --git a/source/blender/io/usd/intern/usd_writer_material.cc b/source/blender/io/usd/intern/usd_writer_material.cc index 599ff0113f7..0c0c78d9432 100644 --- a/source/blender/io/usd/intern/usd_writer_material.cc +++ b/source/blender/io/usd/intern/usd_writer_material.cc @@ -858,16 +858,19 @@ static std::string get_tex_image_asset_filepath(Image *ima) 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_filepath(const USDExporterContext &usd_export_context, bNode *node) { + return get_tex_image_asset_filepath( + node, usd_export_context.stage, usd_export_context.export_params); +} + +std::string get_tex_image_asset_filepath(bNode *node, + const pxr::UsdStageRefPtr stage, + const USDExportParams &export_params) +{ + std::string stage_path = stage->GetRootLayer()->GetRealPath(); + Image *ima = reinterpret_cast(node->id); if (!ima) { return ""; @@ -879,7 +882,7 @@ static std::string get_tex_image_asset_filepath(const USDExporterContext &usd_ex /* Get absolute path. */ path = get_tex_image_asset_filepath(ima); } - else if (usd_export_context.export_params.export_textures) { + 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. */ @@ -890,7 +893,7 @@ static std::string get_tex_image_asset_filepath(const USDExporterContext &usd_ex return path; } - if (usd_export_context.export_params.export_textures) { + if (export_params.export_textures) { /* The texture is exported to a 'textures' directory next to the * USD root layer. */ @@ -898,35 +901,25 @@ static std::string get_tex_image_asset_filepath(const USDExporterContext &usd_ex char file_path[FILE_MAX]; BLI_path_split_file_part(path.c_str(), file_path, FILE_MAX); - if (usd_export_context.export_params.relative_paths) { + if (export_params.relative_paths) { BLI_path_join(exp_path, FILE_MAX, ".", "textures", file_path); } else { /* Create absolute path in the textures directory. */ - std::string export_path = usd_export_context.export_file_path; - if (export_path.empty()) { - return path; - } - char dir_path[FILE_MAX]; - BLI_path_split_dir_part(export_path.c_str(), dir_path, FILE_MAX); + BLI_path_split_dir_part(stage_path.c_str(), dir_path, FILE_MAX); BLI_path_join(exp_path, FILE_MAX, dir_path, "textures", file_path); } BLI_string_replace_char(exp_path, '\\', '/'); return exp_path; } - if (usd_export_context.export_params.relative_paths) { + if (export_params.relative_paths) { /* Get the path relative to the USD. */ - std::string export_path = usd_export_context.export_file_path; - if (export_path.empty()) { - return path; - } - char rel_path[FILE_MAX]; STRNCPY(rel_path, path.c_str()); - BLI_path_rel(rel_path, export_path.c_str()); + BLI_path_rel(rel_path, stage_path.c_str()); if (!BLI_path_is_rel(rel_path)) { return path; } @@ -1028,9 +1021,18 @@ static void copy_single_file(Image *ima, } } -/* Export the given texture node's image to a 'textures' directory in the export path. - * Based on ImagesExporter::export_UV_Image() */ static void export_texture(const USDExporterContext &usd_export_context, bNode *node) +{ + export_texture(node, + usd_export_context.stage, + usd_export_context.export_params.overwrite_textures, + usd_export_context.export_params.worker_status->reports); +} + +void export_texture(bNode *node, + const pxr::UsdStageRefPtr stage, + const bool allow_overwrite, + ReportList *reports) { if (!ELEM(node->type, SH_NODE_TEX_IMAGE, SH_NODE_TEX_ENVIRONMENT)) { return; @@ -1041,7 +1043,7 @@ static void export_texture(const USDExporterContext &usd_export_context, bNode * return; } - std::string export_path = usd_export_context.export_file_path; + std::string export_path = stage->GetRootLayer()->GetRealPath(); if (export_path.empty()) { return; } @@ -1057,21 +1059,17 @@ static void export_texture(const USDExporterContext &usd_export_context, bNode * 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); - const bool allow_overwrite = usd_export_context.export_params.overwrite_textures; std::string dest_dir(tex_dir_path); if (is_generated || is_dirty || is_packed) { - export_in_memory_texture( - ima, dest_dir, allow_overwrite, usd_export_context.export_params.worker_status->reports); + export_in_memory_texture(ima, dest_dir, allow_overwrite, reports); } else if (ima->source == IMA_SRC_TILED) { - copy_tiled_textures( - ima, dest_dir, allow_overwrite, usd_export_context.export_params.worker_status->reports); + copy_tiled_textures(ima, dest_dir, allow_overwrite, reports); } else { - copy_single_file( - ima, dest_dir, allow_overwrite, usd_export_context.export_params.worker_status->reports); + copy_single_file(ima, dest_dir, allow_overwrite, reports); } } diff --git a/source/blender/io/usd/intern/usd_writer_material.hh b/source/blender/io/usd/intern/usd_writer_material.hh index 07409d8fbd8..1a2e5bfe0bc 100644 --- a/source/blender/io/usd/intern/usd_writer_material.hh +++ b/source/blender/io/usd/intern/usd_writer_material.hh @@ -7,12 +7,14 @@ #include +struct bNode; struct Material; struct ReportList; namespace blender::io::usd { struct USDExporterContext; +struct USDExportParams; /* Create USDMaterial from Blender material. * @@ -29,4 +31,22 @@ pxr::UsdShadeMaterial create_usd_material(const USDExporterContext &usd_export_c * or an empty TfToken if the input name is not found in the map. */ const pxr::TfToken token_for_input(const char *input_name); +/* Export the given texture node's image to a 'textures' directory in the given stage's + * export path. */ +void export_texture(bNode *node, + const pxr::UsdStageRefPtr stage, + const bool allow_overwrite = false, + ReportList *reports = nullptr); + +/* 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. */ +std::string get_tex_image_asset_filepath(bNode *node, + const pxr::UsdStageRefPtr stage, + const USDExportParams &export_params); + } // namespace blender::io::usd diff --git a/source/blender/io/usd/usd.hh b/source/blender/io/usd/usd.hh index 10b5b33a02a..17148311690 100644 --- a/source/blender/io/usd/usd.hh +++ b/source/blender/io/usd/usd.hh @@ -112,6 +112,7 @@ struct USDExportParams { bool convert_orientation = false; enum eIOAxis forward_axis = eIOAxis::IO_AXIS_NEGATIVE_Z; enum eIOAxis up_axis = eIOAxis::IO_AXIS_Y; + bool convert_world_material = true; eUSDXformOpMode xform_op_mode = eUSDXformOpMode::USD_XFORM_OP_TRS; bool export_meshes = true; bool export_lights = true; @@ -164,6 +165,7 @@ struct USDImportParams { eUSDTexNameCollisionMode tex_name_collision_mode; bool import_all_materials; eUSDAttrImportMode attr_import_mode; + bool create_world_material; /** * Communication structure between the wmJob management code and the worker code. Currently used diff --git a/tests/python/bl_usd_export_test.py b/tests/python/bl_usd_export_test.py index c350d17a42c..a7826624570 100644 --- a/tests/python/bl_usd_export_test.py +++ b/tests/python/bl_usd_export_test.py @@ -92,6 +92,7 @@ class USDExportTest(AbstractUSDTest): filepath=str(export_path), export_materials=True, evaluation_mode="RENDER", + convert_world_material=False, ) self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}")