From 9afa991316caebfafcfcc55f30b100c72afda0f2 Mon Sep 17 00:00:00 2001 From: Dawid Kurek Date: Mon, 4 Aug 2025 15:58:37 +0200 Subject: [PATCH] Hydra: Handle dome light rotation This refactors the code for world to dome light to be shared between USD and Hydra, and makes rotations work for Hydra the same way they do in USD. One small behavior change is that missing image files now render black, matching Cycles and EEVEE more closely. Co-authored-by: Brecht Van Lommel Pull Request: https://projects.blender.org/blender/blender/pulls/143035 --- source/blender/io/usd/hydra/world.cc | 101 ++----- source/blender/io/usd/hydra/world.hh | 3 - .../io/usd/intern/usd_light_convert.cc | 286 +++++++----------- .../io/usd/intern/usd_light_convert.hh | 26 ++ .../multiple_lights_in_volume.png | 4 +- 5 files changed, 173 insertions(+), 247 deletions(-) diff --git a/source/blender/io/usd/hydra/world.cc b/source/blender/io/usd/hydra/world.cc index 98bd330b78f..037ac1b9f4c 100644 --- a/source/blender/io/usd/hydra/world.cc +++ b/source/blender/io/usd/hydra/world.cc @@ -7,27 +7,23 @@ #include #include +#include #include #include #include #include #include -#include "DNA_node_types.h" #include "DNA_scene_types.h" #include "DNA_world_types.h" -#include "BLI_math_rotation.h" +#include "BLI_math_constants.h" -#include "BKE_node.hh" -#include "BKE_node_legacy_types.hh" -#include "BKE_node_runtime.hh" #include "BKE_studiolight.h" -#include "NOD_shader.h" - #include "hydra_scene_delegate.hh" #include "image.hh" +#include "usd_light_convert.hh" /* TODO: add custom `tftoken` "transparency"? */ @@ -45,71 +41,39 @@ void WorldData::init() { data_.clear(); + pxr::GfVec3f color(1.0f, 1.0f, 1.0f); float intensity = 1.0f; pxr::SdfAssetPath texture_file; if (scene_delegate_->shading_settings.use_scene_world) { const World *world = scene_delegate_->scene->world; - pxr::GfVec3f color(1.0f, 1.0f, 1.0f); ID_LOG("%s", world->id.name); - world->nodetree->ensure_topology_cache(); + usd::WorldToDomeLight res; + usd::world_material_to_dome_light(scene_delegate_->scene, res); - /* TODO: Create nodes parsing system */ - bNode *output_node = ntreeShaderOutputNode(world->nodetree, SHD_OUTPUT_ALL); - if (!output_node) { - return; - } - const Span input_sockets = output_node->input_sockets(); - bNodeSocket *input_socket = nullptr; + if (res.image) { + const std::string file_path = cache_or_get_image_file( + scene_delegate_->bmain, scene_delegate_->scene, res.image, res.iuser); + if (!file_path.empty()) { + texture_file = pxr::SdfAssetPath(file_path, file_path); + } - for (auto *socket : input_sockets) { - if (STREQ(socket->name, "Surface")) { - input_socket = socket; - break; + if (res.mult_found) { + color = pxr::GfVec3f(res.color_mult); } } - if (!input_socket) { - return; + else if (res.color_found) { + const std::string File_path = blender::io::usd::cache_image_color(res.color); + texture_file = pxr::SdfAssetPath(File_path, File_path); + intensity = res.intensity; } - if (input_socket->directly_linked_links().is_empty()) { - return; - } - bNodeLink const *link = input_socket->directly_linked_links()[0]; - - bNode *input_node = link->fromnode; - if (input_node->type_legacy != SH_NODE_BACKGROUND) { - return; + else { + intensity = 0.0f; + color = pxr::GfVec3f(0.0f, 0.0f, 0.0f); } - const bNodeSocket &color_input = *input_node->input_by_identifier("Color"); - const bNodeSocket &strength_input = *input_node->input_by_identifier("Strength"); - - float const *strength = strength_input.default_value_typed(); - float const *input_color = color_input.default_value_typed(); - intensity = strength[1]; - color = pxr::GfVec3f(input_color[0], input_color[1], input_color[2]); - - if (!color_input.directly_linked_links().is_empty()) { - bNode *color_input_node = color_input.directly_linked_links()[0]->fromnode; - if (ELEM(color_input_node->type_legacy, SH_NODE_TEX_IMAGE, SH_NODE_TEX_ENVIRONMENT)) { - NodeTexImage *tex = static_cast(color_input_node->storage); - Image *image = (Image *)color_input_node->id; - if (image) { - std::string image_path = cache_or_get_image_file( - scene_delegate_->bmain, scene_delegate_->scene, image, &tex->iuser); - if (!image_path.empty()) { - texture_file = pxr::SdfAssetPath(image_path, image_path); - } - } - } - } - - if (texture_file.GetAssetPath().empty()) { - float fill_color[4] = {color[0], color[1], color[2], 1.0f}; - std::string image_path = blender::io::usd::cache_image_color(fill_color); - texture_file = pxr::SdfAssetPath(image_path, image_path); - } + transform = res.transform; } else { ID_LOG("studiolight: %s", scene_delegate_->shading_settings.studiolight_name.c_str()); @@ -119,18 +83,20 @@ void WorldData::init() STUDIOLIGHT_ORIENTATIONS_MATERIAL_MODE); if (sl != nullptr && sl->flag & STUDIOLIGHT_TYPE_WORLD) { texture_file = pxr::SdfAssetPath(sl->filepath, sl->filepath); - /* coefficient to follow Cycles result */ + /* Coefficient to follow Cycles result */ intensity = scene_delegate_->shading_settings.studiolight_intensity / 2; } + + transform = pxr::GfMatrix4d().SetRotate( + pxr::GfRotation(pxr::GfVec3d(0.0, 0.0, -1.0), + RAD2DEGF(scene_delegate_->shading_settings.studiolight_rotation))); } data_[pxr::UsdLuxTokens->orientToStageUpAxis] = true; data_[pxr::HdLightTokens->intensity] = intensity; data_[pxr::HdLightTokens->exposure] = 0.0f; - data_[pxr::HdLightTokens->color] = pxr::GfVec3f(1.0f, 1.0f, 1.0f); + data_[pxr::HdLightTokens->color] = color; data_[pxr::HdLightTokens->textureFile] = texture_file; - - write_transform(); } void WorldData::update() @@ -154,15 +120,4 @@ void WorldData::update() } } -void WorldData::write_transform() -{ - transform = 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)); - if (!scene_delegate_->shading_settings.use_scene_world) { - transform *= pxr::GfMatrix4d().SetRotate( - pxr::GfRotation(pxr::GfVec3d(0.0, 0.0, -1.0), - RAD2DEGF(scene_delegate_->shading_settings.studiolight_rotation))); - } -} - } // namespace blender::io::hydra diff --git a/source/blender/io/usd/hydra/world.hh b/source/blender/io/usd/hydra/world.hh index ebd2c1fc1c3..09f5cc2d56b 100644 --- a/source/blender/io/usd/hydra/world.hh +++ b/source/blender/io/usd/hydra/world.hh @@ -20,9 +20,6 @@ class WorldData : public LightData { void init() override; void update() override; - - protected: - void write_transform() override; }; } // namespace blender::io::hydra diff --git a/source/blender/io/usd/intern/usd_light_convert.cc b/source/blender/io/usd/intern/usd_light_convert.cc index 7a0d872d0b4..046c16a4ddf 100644 --- a/source/blender/io/usd/intern/usd_light_convert.cc +++ b/source/blender/io/usd/intern/usd_light_convert.cc @@ -53,26 +53,11 @@ static const pxr::TfToken pole_axis_z("Z", pxr::TfToken::Immortal); namespace { -/** - * Helper struct for retrieving shader information when traversing a world material - * node chain, provided as user data for #bke::node_chain_iterator(). - */ -struct WorldNtreeSearchResults { +struct WorldNtreeSearchPayload { const blender::io::usd::USDExportParams ¶ms; pxr::UsdStageRefPtr stage; - std::string file_path; - - float world_intensity = 0.0f; - float world_color[3]{}; - float mapping_rot[3]{}; - float color_mult[3]{}; - - bool background_found = false; - bool env_tex_found = false; - bool mult_found = false; - - WorldNtreeSearchResults(const blender::io::usd::USDExportParams &in_params, + WorldNtreeSearchPayload(const blender::io::usd::USDExportParams &in_params, pxr::UsdStageRefPtr in_stage) : params(in_params), stage(in_stage) { @@ -153,76 +138,6 @@ static bNode *append_node(bNode *dst_node, 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_legacy == SH_NODE_BACKGROUND) { - /* Get light color and intensity */ - const bNodeSocketValueRGBA *color_data = bke::node_find_socket(*fromnode, SOCK_IN, "Color") - ->default_value_typed(); - const bNodeSocketValueFloat *strength_data = - bke::node_find_socket(*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_legacy == 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_legacy == SH_NODE_VECTOR_MATH) - { - - if (fromnode->custom1 == NODE_VECTOR_MATH_MULTIPLY) { - res->mult_found = true; - - bNodeSocket *vec_sock = bke::node_find_socket(*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_legacy == SH_NODE_MAPPING) { - copy_v3_fl(res->mapping_rot, 0.0f); - if (bNodeSocket *socket = bke::node_find_socket(*fromnode, SOCK_IN, "Rotation")) { - const bNodeSocketValueVector *rot_value = static_cast( - socket->default_value); - copy_v3_v3(res->mapping_rot, rot_value->value); - } - } - - return true; -} - void world_material_to_dome_light(const USDExportParams ¶ms, const Scene *scene, pxr::UsdStageRefPtr stage) @@ -231,58 +146,57 @@ void world_material_to_dome_light(const USDExportParams ¶ms, return; } - WorldNtreeSearchResults res(params, stage); + WorldToDomeLight res; + world_material_to_dome_light(scene, res); - if (scene->world->nodetree) { - /* Find the world output. */ - bNode *output = nullptr; - const bNodeTree *ntree = scene->world->nodetree; - ntree->ensure_topology_cache(); - const Span bsdf_nodes = ntree->nodes_by_type("ShaderNodeOutputWorld"); - for (const bNode *node : bsdf_nodes) { - if (node->flag & NODE_DO_OUTPUT) { - output = const_cast(node); - break; - } - } - - if (!output) { - /* No output, no valid network to convert. */ - return; - } - - bke::node_chain_iterator(scene->world->nodetree, output, node_search, &res, true); - } - else { - res.world_intensity = 1.0f; - zero_v3(res.world_color); - res.background_found = false; - } - - if (!(res.background_found || res.env_tex_found)) { + if (!(res.color_found || res.image)) { /* No nodes to convert */ return; } + std::string image_filepath; + if (res.image) { + /* Compute image filepath and export if needed. */ + image_filepath = get_tex_image_asset_filepath(res.image, stage, params); + if (image_filepath.empty()) { + return; + } + if (params.export_textures) { + export_texture(res.image, stage, params.overwrite_textures); + } + } + /* Create USD dome light. */ - pxr::SdfPath env_light_path = get_unique_path(stage, params.root_prim_path + "/env_light"); - pxr::UsdLuxDomeLight dome_light = pxr::UsdLuxDomeLight::Define(stage, env_light_path); - if (!res.env_tex_found) { - /* Like the Hydra delegate, if no texture is found export a solid - * color texture as a stand-in so that Hydra renderers don't - * throw errors. */ + if (res.image) { + /* Use existing image texture file. */ + dome_light.CreateTextureFileAttr().Set(pxr::SdfAssetPath(image_filepath)); - float fill_color[4] = {res.world_color[0], res.world_color[1], res.world_color[2], 1.0f}; + /* Set optional color multiplication. */ + 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); + } - std::string source_path = cache_image_color(fill_color); + /* Set transform. */ + pxr::GfVec3d angles = res.transform.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); + } + else if (res.color_found) { + /* If no texture is found export a solid color texture as a stand-in so that Hydra + * renderers don't throw errors. */ + dome_light.CreateIntensityAttr().Set(res.intensity); + + std::string source_path = cache_image_color(res.color); const std::string base_path = stage->GetRootLayer()->GetRealPath(); - /* It'll be short, coming from cache_image_color. */ - char file_path[64]; - BLI_path_split_file_part(source_path.c_str(), file_path, 64); + char file_path[FILE_MAX]; + BLI_path_split_file_part(source_path.c_str(), file_path, FILE_MAX); char dest_path[FILE_MAX]; BLI_path_split_dir_part(base_path.c_str(), dest_path, FILE_MAX); @@ -295,52 +209,11 @@ void world_material_to_dome_light(const USDExportParams ¶ms, CLOG_WARN(&LOG, "USD Export: Couldn't write world color image to %s", dest_path); } else { - res.env_tex_found = true; BLI_path_join(dest_path, FILE_MAX, ".", "textures", file_path); - res.file_path = dest_path; + image_filepath = dest_path; + dome_light.CreateTextureFileAttr().Set(pxr::SdfAssetPath(image_filepath)); } } - - 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 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. */ @@ -531,4 +404,79 @@ void dome_light_to_world_material(const USDImportParams ¶ms, BKE_ntree_update_after_single_tree_change(*bmain, *ntree); } +static bool node_search(bNode *fromnode, bNode * /*tonode*/, void *userdata, bool /*reversed*/) +{ + if (!(userdata && fromnode)) { + return true; + } + + WorldToDomeLight &res = *static_cast(userdata); + + if (!res.color_found && fromnode->type_legacy == SH_NODE_BACKGROUND) { + /* Get light color and intensity */ + const bNodeSocketValueRGBA *color_data = bke::node_find_socket(*fromnode, SOCK_IN, "Color") + ->default_value_typed(); + const bNodeSocketValueFloat *strength_data = + bke::node_find_socket(*fromnode, SOCK_IN, "Strength") + ->default_value_typed(); + + res.color_found = true; + res.intensity = strength_data->value; + res.color[0] = color_data->value[0]; + res.color[1] = color_data->value[1]; + res.color[2] = color_data->value[2]; + res.color[3] = 1.0f; + } + else if (!res.image && fromnode->type_legacy == SH_NODE_TEX_ENVIRONMENT) { + NodeTexImage *tex = static_cast(fromnode->storage); + res.image = reinterpret_cast(fromnode->id); + res.iuser = &tex->iuser; + } + else if (!res.image && !res.mult_found && fromnode->type_legacy == SH_NODE_VECTOR_MATH) { + if (fromnode->custom1 == NODE_VECTOR_MATH_MULTIPLY) { + res.mult_found = true; + + bNodeSocket *vec_sock = bke::node_find_socket(*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.image && fromnode->type_legacy == SH_NODE_MAPPING) { + if (bNodeSocket *socket = bke::node_find_socket(*fromnode, SOCK_IN, "Rotation")) { + const bNodeSocketValueVector *rot_value = static_cast( + socket->default_value); + /* Convert radians to degrees. */ + pxr::GfVec3f rot(rot_value->value); + mul_v3_fl(rot.data(), 180.0f / M_PI); + res.transform = + 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), -rot[2])) * + pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(0.0, 1.0, 0.0), -rot[1])) * + pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(1.0, 0.0, 0.0), -rot[0])); + } + } + return true; +} + +void world_material_to_dome_light(const Scene *scene, WorldToDomeLight &res) +{ + /* Find the world output. */ + scene->world->nodetree->ensure_topology_cache(); + const Span bsdf_nodes = scene->world->nodetree->nodes_by_type( + "ShaderNodeOutputWorld"); + + for (const bNode *node : bsdf_nodes) { + if (node->flag & NODE_DO_OUTPUT) { + bke::node_chain_iterator(scene->world->nodetree, node, node_search, &res, true); + break; + } + } +} + } // 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 index cf5f9ee62f6..a1050c7a550 100644 --- a/source/blender/io/usd/intern/usd_light_convert.hh +++ b/source/blender/io/usd/intern/usd_light_convert.hh @@ -6,6 +6,11 @@ #include #include +struct bNode; +struct bNodeTree; + +struct Image; +struct ImageUser; struct Main; struct Scene; @@ -41,4 +46,25 @@ void dome_light_to_world_material(const USDImportParams ¶ms, const pxr::UsdPrim &prim, const pxr::UsdTimeCode time = 0.0); +/** + * Helper struct for converting world shader nodes to a dome light, used by both + * USD and Hydra. */ +struct WorldToDomeLight { + /* Image and its transform. */ + Image *image = nullptr; + ImageUser *iuser = nullptr; + pxr::GfMatrix4d transform = pxr::GfMatrix4d(1.0); + + /* Multiply image by color. */ + bool mult_found = false; + float color_mult[4]{}; + + /* Fixed color. */ + bool color_found = false; + float intensity = 0.0f; + float color[4]{}; +}; + +void world_material_to_dome_light(const Scene *scene, WorldToDomeLight &res); + } // namespace blender::io::usd diff --git a/tests/files/render/light/storm_hydra_renders/multiple_lights_in_volume.png b/tests/files/render/light/storm_hydra_renders/multiple_lights_in_volume.png index 58e37a2b598..04b2db636dd 100644 --- a/tests/files/render/light/storm_hydra_renders/multiple_lights_in_volume.png +++ b/tests/files/render/light/storm_hydra_renders/multiple_lights_in_volume.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ade76348621bfdd9fa1ce64e33cd7da758cd09630b368bf92d4e6cac4c8c0ab -size 1091 +oid sha256:d7f618192325eb1ab7c0d9f001ab1a2ba246e0c2379036fe1f2f082a615a3a10 +size 896