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 <charles@skeletalstudios.com>
Co-authored-by: Michael Kowalski <makowalski@nvidia.com>
Pull Request: https://projects.blender.org/blender/blender/pulls/121800
This commit is contained in:
Charles Wardlaw
2024-05-30 20:48:43 +02:00
committed by Michael Kowalski
parent 3ed825f981
commit e1a6749b3d
15 changed files with 737 additions and 52 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -10,10 +10,11 @@
#include <pxr/usd/ar/resolver.h>
#include <pxr/usd/ar/writableAsset.h>
#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

View File

@@ -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

View File

@@ -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 <pxr/base/tf/token.h>
@@ -342,6 +343,13 @@ pxr::UsdStageRefPtr export_to_stage(const USDExportParams &params,
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

View File

@@ -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);
}

View File

@@ -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 <pxr/base/gf/matrix4f.h>
#include <pxr/base/gf/rotation.h>
#include <pxr/base/gf/vec3f.h>
#include <pxr/usd/ar/packageUtils.h>
#include <pxr/usd/usdGeom/scope.h>
#include <pxr/usd/usdGeom/xformCache.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <pxr/usd/usdLux/domeLight.h>
#include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdShade/materialBindingAPI.h>
#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 <string>
#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<typename T>
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<T>(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<T>(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 &params)
{
/* 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<WorldNtreeSearchResults *>(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<bNodeSocketValueRGBA>();
bNodeSocketValueFloat *strength_data = bke::nodeFindSocket(fromnode, SOCK_IN, "Strength")
->default_value_typed<bNodeSocketValueFloat>();
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<bNodeSocketValueVector *>(
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 &params,
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<const bNode *> 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 &params,
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<bNodeSocketValueVector *>(
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

View File

@@ -0,0 +1,31 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/usdLux/domeLight.h>
struct Light;
struct Main;
struct Scene;
namespace blender::io::usd {
struct USDExportParams;
struct USDImportParams;
struct ImportSettings;
void world_material_to_dome_light(const USDExportParams &params,
const Scene *scene,
pxr::UsdStageRefPtr stage);
void dome_light_to_world_material(const USDImportParams &params,
const ImportSettings &settings,
Scene *scene,
Main *bmain,
const pxr::UsdLuxDomeLight &dome_light,
const double time = 0.0);
} // namespace blender::io::usd

View File

@@ -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;
/**

View File

@@ -181,6 +181,10 @@ USDPrimReader *USDStageReader::create_reader_if_allowed(const pxr::UsdPrim &prim
if (params_.import_meshes && prim.IsA<pxr::UsdGeomMesh>()) {
return new USDMeshReader(prim, params_, settings_);
}
if (params_.import_lights && prim.IsA<pxr::UsdLuxDomeLight>()) {
/* Dome lights are handled elsewhere. */
return nullptr;
}
#if PXR_VERSION >= 2111
if (params_.import_lights &&
(prim.IsA<pxr::UsdLuxBoundableLightBase>() || prim.IsA<pxr::UsdLuxNonboundableLightBase>()))
@@ -226,6 +230,10 @@ USDPrimReader *USDStageReader::create_reader(const pxr::UsdPrim &prim)
if (prim.IsA<pxr::UsdGeomMesh>()) {
return new USDMeshReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdLuxDomeLight>()) {
/* We don't handle dome lights. */
return nullptr;
}
#if PXR_VERSION >= 2111
if (prim.IsA<pxr::UsdLuxBoundableLightBase>() || prim.IsA<pxr::UsdLuxNonboundableLightBase>()) {
#else
@@ -364,6 +372,10 @@ USDPrimReader *USDStageReader::collect_readers(const pxr::UsdPrim &prim,
}
}
if (prim.IsA<pxr::UsdLuxDomeLight>()) {
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. */

View File

@@ -12,6 +12,7 @@
#include "usd_reader_prim.hh"
#include <pxr/usd/usdGeom/imageable.h>
#include <pxr/usd/usdLux/domeLight.h>
#include <string>
@@ -41,6 +42,10 @@ class USDStageReader {
blender::Vector<USDPrimReader *> readers_;
/* USD dome lights are converted to a world material,
* rather than light objects, so are handled differently */
blender::Vector<pxr::UsdLuxDomeLight> dome_lights_;
/* USD material prim paths encountered during stage
* traversal, for importing unused materials. */
blender::Vector<std::string> material_paths_;
@@ -112,6 +117,11 @@ class USDStageReader {
return readers_;
};
const blender::Vector<pxr::UsdLuxDomeLight> &dome_lights() const
{
return dome_lights_;
};
void sort_readers();
/**

View File

@@ -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<Image *>(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);
}
}

View File

@@ -7,12 +7,14 @@
#include <string>
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

View File

@@ -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

View File

@@ -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}")