diff --git a/source/blender/io/usd/CMakeLists.txt b/source/blender/io/usd/CMakeLists.txt index 4acafaaeb84..e401aa5785c 100644 --- a/source/blender/io/usd/CMakeLists.txt +++ b/source/blender/io/usd/CMakeLists.txt @@ -89,6 +89,7 @@ set(SRC intern/usd_writer_volume.cc intern/usd_reader_camera.cc + intern/usd_reader_domelight.cc intern/usd_reader_curve.cc intern/usd_reader_geom.cc intern/usd_reader_instance.cc @@ -140,6 +141,7 @@ set(SRC intern/usd_reader_camera.hh intern/usd_reader_curve.hh + intern/usd_reader_domelight.hh intern/usd_reader_geom.hh intern/usd_reader_instance.hh intern/usd_reader_light.hh diff --git a/source/blender/io/usd/intern/usd_capi_import.cc b/source/blender/io/usd/intern/usd_capi_import.cc index 1650c6be188..6720b5ce274 100644 --- a/source/blender/io/usd/intern/usd_capi_import.cc +++ b/source/blender/io/usd/intern/usd_capi_import.cc @@ -5,7 +5,7 @@ #include "IO_types.hh" #include "usd.hh" #include "usd_hook.hh" -#include "usd_light_convert.hh" +#include "usd_reader_domelight.hh" #include "usd_reader_geom.hh" #include "usd_reader_prim.hh" #include "usd_reader_stage.hh" @@ -236,10 +236,10 @@ 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()) + !archive->dome_light_readers().is_empty()) { - dome_light_to_world_material( - data->params, data->scene, data->bmain, archive->dome_lights().first()); + USDDomeLightReader *dome_light_reader = archive->dome_light_readers().first(); + dome_light_reader->create_object(data->scene, data->bmain); } if (data->params.import_materials && data->params.import_all_materials) { diff --git a/source/blender/io/usd/intern/usd_light_convert.cc b/source/blender/io/usd/intern/usd_light_convert.cc index dcf67f9f90e..9b635766265 100644 --- a/source/blender/io/usd/intern/usd_light_convert.cc +++ b/source/blender/io/usd/intern/usd_light_convert.cc @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include "BKE_image.hh" #include "BKE_library.hh" @@ -42,47 +44,12 @@ 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); +// Attribute values. +static const pxr::TfToken pole_axis_z("Z", 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 attributes did not have the - * "inputs:" prefix. One can provide the older input attribute 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, - const pxr::UsdPrim &prim, - const pxr::TfToken fallback_attr_name, - T *r_value) -{ - 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::node_chain_iterator(). @@ -377,10 +344,11 @@ void world_material_to_dome_light(const USDExportParams ¶ms, void dome_light_to_world_material(const USDImportParams ¶ms, Scene *scene, Main *bmain, - const pxr::UsdLuxDomeLight &dome_light, + const USDImportDomeLightData &dome_light_data, + const pxr::UsdPrim &prim, const double motionSampleTime) { - if (!(scene && scene->world && dome_light)) { + if (!(scene && scene->world && prim)) { return; } @@ -451,36 +419,18 @@ void dome_light_to_world_material(const USDImportParams ¶ms, } /* Set the background shader intensity. */ - float intensity = 1.0f; - get_authored_value(dome_light.GetIntensityAttr(), - motionSampleTime, - dome_light.GetPrim(), - usdtokens::intensity, - &intensity); - - intensity *= params.light_intensity_scale; + float intensity = dome_light_data.intensity * params.light_intensity_scale; bNodeSocket *strength_sock = bke::node_find_socket(*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(), - motionSampleTime, - dome_light.GetPrim(), - usdtokens::texture_file, - &tex_path); - - pxr::GfVec3f color; - bool has_color = get_authored_value( - dome_light.GetColorAttr(), motionSampleTime, dome_light.GetPrim(), usdtokens::color, &color); - - if (!has_tex) { + if (!dome_light_data.has_tex) { /* No texture file is authored on the dome light. Set the color, if it was authored, * and return early. */ - if (has_color) { + if (dome_light_data.has_color) { bNodeSocket *color_sock = bke::node_find_socket(*bgshader, SOCK_IN, "Color"); - copy_v3_v3(((bNodeSocketValueRGBA *)color_sock->default_value)->value, color.data()); + copy_v3_v3(((bNodeSocketValueRGBA *)color_sock->default_value)->value, + dome_light_data.color.data()); } bke::node_set_active(*ntree, *output); @@ -493,7 +443,7 @@ void dome_light_to_world_material(const USDImportParams ¶ms, * texture output. */ bNode *mult = nullptr; - if (has_color) { + if (dome_light_data.has_color) { mult = append_node(bgshader, SH_NODE_VECTOR_MATH, "Vector", "Color", ntree, 200); if (!mult) { @@ -510,7 +460,8 @@ void dome_light_to_world_material(const USDImportParams ¶ms, } if (vec_sock) { - copy_v3_v3(((bNodeSocketValueVector *)vec_sock->default_value)->value, color.data()); + copy_v3_v3(((bNodeSocketValueVector *)vec_sock->default_value)->value, + dome_light_data.color.data()); } else { CLOG_WARN(&LOG, "Couldn't find vector multiply second vector socket"); @@ -547,10 +498,12 @@ void dome_light_to_world_material(const USDImportParams ¶ms, } /* Load the texture image. */ - std::string resolved_path = tex_path.GetResolvedPath(); + std::string resolved_path = dome_light_data.tex_path.GetResolvedPath(); if (resolved_path.empty()) { - CLOG_WARN(&LOG, "Couldn't get resolved path for asset %s", tex_path.GetAssetPath().c_str()); + CLOG_WARN(&LOG, + "Couldn't get resolved path for asset %s", + dome_light_data.tex_path.GetAssetPath().c_str()); return; } @@ -564,21 +517,34 @@ void dome_light_to_world_material(const USDImportParams ¶ms, /* Set the transform. */ pxr::UsdGeomXformCache xf_cache(motionSampleTime); - pxr::GfMatrix4d xf = xf_cache.GetLocalToWorldTransform(dome_light.GetPrim()); + pxr::GfMatrix4d xf = xf_cache.GetLocalToWorldTransform(prim); - pxr::UsdStageRefPtr stage = dome_light.GetPrim().GetStage(); + pxr::UsdStageRefPtr stage = prim.GetStage(); if (!stage) { - CLOG_WARN( - &LOG, "Couldn't get stage for dome light %s", dome_light.GetPrim().GetPath().GetText()); + CLOG_WARN(&LOG, "Couldn't get stage for dome light %s", prim.GetPath().GetText()); return; } - if (pxr::UsdGeomGetStageUpAxis(stage) == pxr::UsdGeomTokens->y) { + /* Note: This logic tries to produce identical results to `usdview` as of USD 25.05. + * However, `usdview` seems to handle Y-Up stages differently; some scenes match while others + * do not unless we keep the second conditional below (+90 on x-axis). */ + const pxr::TfToken stage_up = pxr::UsdGeomGetStageUpAxis(stage); + const bool needs_stage_z_adjust = stage_up == pxr::UsdGeomTokens->z && + ELEM(dome_light_data.pole_axis, + pxr::UsdLuxTokens->Z, + pxr::UsdLuxTokens->scene); + const bool needs_stage_y_adjust = stage_up == pxr::UsdGeomTokens->y && + ELEM(dome_light_data.pole_axis, pxr::UsdLuxTokens->Z); + if (needs_stage_z_adjust || needs_stage_y_adjust) { + xf *= pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(0.0, 1.0, 0.0), 90.0)); + } + else if (stage_up == pxr::UsdGeomTokens->y) { /* Convert from Y-up to Z-up with a 90 degree rotation about the X-axis. */ xf *= pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(1.0, 0.0, 0.0), 90.0)); } + /* Rotate into Blender's frame of reference. */ 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; diff --git a/source/blender/io/usd/intern/usd_light_convert.hh b/source/blender/io/usd/intern/usd_light_convert.hh index c4ff9fd2722..a49b283da40 100644 --- a/source/blender/io/usd/intern/usd_light_convert.hh +++ b/source/blender/io/usd/intern/usd_light_convert.hh @@ -4,7 +4,6 @@ #pragma once #include -#include struct Main; struct Scene; @@ -14,6 +13,18 @@ namespace blender::io::usd { struct USDExportParams; struct USDImportParams; +/* This struct contains all DomeLight attribute needed to + * create a world environment */ +struct USDImportDomeLightData { + float intensity; + pxr::GfVec3f color; + pxr::SdfAssetPath tex_path; + pxr::TfToken pole_axis; + + bool has_color; + bool has_tex; +}; + /** * If the Blender scene has an environment texture, * export it as a USD dome light. @@ -25,7 +36,8 @@ void world_material_to_dome_light(const USDExportParams ¶ms, void dome_light_to_world_material(const USDImportParams ¶ms, Scene *scene, Main *bmain, - const pxr::UsdLuxDomeLight &dome_light, + const USDImportDomeLightData &dome_light_data, + const pxr::UsdPrim &prim, const double motionSampleTime = 0.0); } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_reader_domelight.cc b/source/blender/io/usd/intern/usd_reader_domelight.cc new file mode 100644 index 00000000000..92bb96bad31 --- /dev/null +++ b/source/blender/io/usd/intern/usd_reader_domelight.cc @@ -0,0 +1,120 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "usd_reader_domelight.hh" +#include "usd_light_convert.hh" + +#include +#include +#include + +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); +static const pxr::TfToken pole_axis("poleAxis", pxr::TfToken::Immortal); +} // namespace usdtokens + +namespace blender::io::usd { + +/** + * 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 attributes did not have the + * "inputs:" prefix. One can provide the older input attribute 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 +static bool get_authored_value(const pxr::UsdAttribute &attr, + const double motionSampleTime, + const pxr::UsdPrim &prim, + const pxr::TfToken fallback_attr_name, + T *r_value) +{ + 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; +} + +template static float get_intensity(const T &dome_light, float motionSampleTime) +{ + float intensity = 1.0f; + get_authored_value(dome_light.GetIntensityAttr(), + motionSampleTime, + dome_light.GetPrim(), + usdtokens::intensity, + &intensity); + return intensity; +} + +template +static bool get_tex_path(const T &dome_light, float motionSampleTime, pxr::SdfAssetPath *tex_path) +{ + bool has_tex = get_authored_value(dome_light.GetTextureFileAttr(), + motionSampleTime, + dome_light.GetPrim(), + usdtokens::texture_file, + tex_path); + return has_tex; +} + +template +static bool get_color(const T &dome_light, float motionSampleTime, pxr::GfVec3f *color) +{ + bool has_color = get_authored_value( + dome_light.GetColorAttr(), motionSampleTime, dome_light.GetPrim(), usdtokens::color, color); + return has_color; +} + +static pxr::TfToken get_pole_axis(const pxr::UsdLuxDomeLight_1 &dome_light, float motionSampleTime) +{ + pxr::TfToken pole_axis = pxr::UsdLuxTokens->scene; + get_authored_value( + dome_light.GetPoleAxisAttr(), motionSampleTime, dome_light.GetPrim(), {}, &pole_axis); + return pole_axis; +} + +void USDDomeLightReader::create_object(Scene *scene, Main *bmain) +{ + USDImportDomeLightData dome_light_data; + + /* Time varying dome lights are not currently supported. */ + const double motionSampleTime = 0.0; + + if (prim_.IsA()) { + pxr::UsdLuxDomeLight dome_light = pxr::UsdLuxDomeLight(prim_); + dome_light_data.intensity = get_intensity(dome_light, motionSampleTime); + dome_light_data.has_tex = get_tex_path( + dome_light, motionSampleTime, &dome_light_data.tex_path); + dome_light_data.has_color = get_color(dome_light, motionSampleTime, &dome_light_data.color); + dome_light_data.pole_axis = pxr::UsdLuxTokens->Y; + } + else if (prim_.IsA()) { + pxr::UsdLuxDomeLight_1 dome_light = pxr::UsdLuxDomeLight_1(prim_); + dome_light_data.intensity = get_intensity(dome_light, motionSampleTime); + dome_light_data.has_tex = get_tex_path( + dome_light, motionSampleTime, &dome_light_data.tex_path); + dome_light_data.has_color = get_color(dome_light, motionSampleTime, &dome_light_data.color); + dome_light_data.pole_axis = get_pole_axis(dome_light, motionSampleTime); + } + + dome_light_to_world_material(import_params_, scene, bmain, dome_light_data, prim_); +} + +} // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_reader_domelight.hh b/source/blender/io/usd/intern/usd_reader_domelight.hh new file mode 100644 index 00000000000..13183971c56 --- /dev/null +++ b/source/blender/io/usd/intern/usd_reader_domelight.hh @@ -0,0 +1,39 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include "usd.hh" +#include "usd_reader_prim.hh" + +#include +#include + +struct Main; +struct Scene; + +namespace blender::io::usd { + +class USDDomeLightReader : public USDPrimReader { + + public: + USDDomeLightReader(const pxr::UsdPrim &prim, + const USDImportParams &import_params, + const ImportSettings &settings) + : USDPrimReader(prim, import_params, settings) + { + } + + bool valid() const override + { + return prim_.IsA() || prim_.IsA(); + } + + /* Until Blender supports DomeLight objects natively, use a separate create_object overload that + * allows the caller to pass in the required Scene data. */ + + void create_object(Main * /*bmain*/) override{}; + void create_object(Scene *scene, Main *bmain); +}; + +} // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_reader_stage.cc b/source/blender/io/usd/intern/usd_reader_stage.cc index c8c44cf2616..e8f7b8ee890 100644 --- a/source/blender/io/usd/intern/usd_reader_stage.cc +++ b/source/blender/io/usd/intern/usd_reader_stage.cc @@ -40,6 +40,8 @@ #include #include #include +#include +#include #include #include @@ -252,7 +254,9 @@ 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()) { + if (params_.import_lights && + (prim.IsA() || prim.IsA())) + { /* Dome lights are handled elsewhere. */ return nullptr; } @@ -297,7 +301,7 @@ USDPrimReader *USDStageReader::create_reader(const pxr::UsdPrim &prim) if (prim.IsA()) { return new USDMeshReader(prim, params_, settings_); } - if (prim.IsA()) { + if (prim.IsA() || prim.IsA()) { /* We don't handle dome lights. */ return nullptr; } @@ -440,8 +444,10 @@ USDPrimReader *USDStageReader::collect_readers(const pxr::UsdPrim &prim, } } - if (prim.IsA()) { - dome_lights_.append(pxr::UsdLuxDomeLight(prim)); + if (prim.IsA() || prim.IsA()) { + USDDomeLightReader *reader = new USDDomeLightReader(prim, params_, settings_); + reader->incref(); + dome_light_readers_.append(reader); } pxr::Usd_PrimFlagsConjunction filter_flags = pxr::UsdPrimIsActive && pxr::UsdPrimIsLoaded && @@ -529,7 +535,6 @@ 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. */ @@ -733,6 +738,11 @@ void USDStageReader::clear_readers() } } instancer_proto_readers_.clear(); + + for (USDDomeLightReader *reader : dome_light_readers_) { + decref(reader); + } + dome_light_readers_.clear(); } void USDStageReader::sort_readers() diff --git a/source/blender/io/usd/intern/usd_reader_stage.hh b/source/blender/io/usd/intern/usd_reader_stage.hh index 8d15d8c8f5a..d4806fa6b51 100644 --- a/source/blender/io/usd/intern/usd_reader_stage.hh +++ b/source/blender/io/usd/intern/usd_reader_stage.hh @@ -9,10 +9,10 @@ #include "usd.hh" #include "usd_hash_types.hh" +#include "usd_reader_domelight.hh" #include "usd_reader_prim.hh" #include -#include struct Collection; struct ImportSettings; @@ -42,7 +42,7 @@ class USDStageReader { /* USD dome lights are converted to a world material, * rather than light objects, so are handled differently */ - blender::Vector dome_lights_; + blender::Vector dome_light_readers_; /* USD material prim paths encountered during stage * traversal, for importing unused materials. */ @@ -128,9 +128,9 @@ class USDStageReader { return readers_; }; - const blender::Vector &dome_lights() const + const blender::Vector &dome_light_readers() const { - return dome_lights_; + return dome_light_readers_; }; void sort_readers(); diff --git a/tests/files/usd/usd_dome_light_1_stageY_poleDefault.usda b/tests/files/usd/usd_dome_light_1_stageY_poleDefault.usda new file mode 100644 index 00000000000..3f1e3bea010 --- /dev/null +++ b/tests/files/usd/usd_dome_light_1_stageY_poleDefault.usda @@ -0,0 +1,123 @@ +#usda 1.0 +( + defaultPrim = "root" + doc = "Blender v4.5.0 Alpha" + metersPerUnit = 1 + upAxis = "Y" +) + +def Xform "root" ( + customData = { + dictionary Blender = { + bool generated = 1 + } + } +) +{ + def DomeLight_1 "env_light" + { + float inputs:intensity = 1 + asset inputs:texture:file = @./textures/test_single.png@ + float3 xformOp:rotateXYZ = (90, 0, 90) + uniform token[] xformOpOrder = ["xformOp:rotateXYZ"] + } + + def Xform "Cube" + { + def Mesh "Cube" ( + active = true + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + uniform bool doubleSided = 1 + float3[] extent = [(-1, -1, -1), (1, 1, 1)] + int[] faceVertexCounts = [4, 4, 4, 4, 4, 4] + int[] faceVertexIndices = [0, 4, 6, 2, 3, 2, 6, 7, 7, 6, 4, 5, 5, 1, 3, 7, 1, 0, 2, 3, 5, 4, 0, 1] + rel material:binding = + normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1), (1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0)] ( + interpolation = "faceVarying" + ) + point3f[] points = [(1, 1, 1), (1, 1, -1), (1, -1, 1), (1, -1, -1), (-1, 1, 1), (-1, 1, -1), (-1, -1, 1), (-1, -1, -1)] + bool[] primvars:sharp_face = [1, 1, 1, 1, 1, 1] ( + interpolation = "uniform" + ) + texCoord2f[] primvars:st = [(0.625, 0.5), (0.875, 0.5), (0.875, 0.75), (0.625, 0.75), (0.375, 0.75), (0.625, 0.75), (0.625, 1), (0.375, 1), (0.375, 0), (0.625, 0), (0.625, 0.25), (0.375, 0.25), (0.125, 0.5), (0.375, 0.5), (0.375, 0.75), (0.125, 0.75), (0.375, 0.5), (0.625, 0.5), (0.625, 0.75), (0.375, 0.75), (0.375, 0.25), (0.625, 0.25), (0.625, 0.5), (0.375, 0.5)] ( + interpolation = "faceVarying" + ) + uniform token subdivisionScheme = "none" + } + } + + def Xform "Cube_001" + { + float3 xformOp:rotateXYZ = (0, -0, 0) + float3 xformOp:scale = (0.2904613, 0.2904613, 0.2904613) + double3 xformOp:translate = (0, 0, 1.6628384590148926) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"] + + def Mesh "Cube_001" ( + active = true + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + uniform bool doubleSided = 1 + float3[] extent = [(-1, -1, -1), (1, 1, 0.9999993)] + int[] faceVertexCounts = [4, 4, 4, 4, 4, 4] + int[] faceVertexIndices = [0, 4, 6, 2, 3, 2, 6, 7, 7, 6, 4, 5, 5, 1, 3, 7, 1, 0, 2, 3, 5, 4, 0, 1] + rel material:binding = + normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, -0.9351529, 0.3542444), (0, -0.9351529, 0.3542444), (0, -0.9351529, 0.3542444), (0, -0.9351529, 0.3542444), (-0.9351529, 0, 0.35424438), (-0.9351529, 0, 0.35424438), (-0.9351529, 0, 0.35424438), (-0.9351529, 0, 0.35424438), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0.9351529, 0, 0.35424438), (0.9351529, 0, 0.35424438), (0.9351529, 0, 0.35424438), (0.9351529, 0, 0.35424438), (0, 0.9351529, 0.3542444), (0, 0.9351529, 0.3542444), (0, 0.9351529, 0.3542444), (0, 0.9351529, 0.3542444)] ( + interpolation = "faceVarying" + ) + point3f[] points = [(0.24238217, 0.24238217, 0.9999993), (1, 1, -1), (0.24238217, -0.24238217, 0.9999993), (1, -1, -1), (-0.24238217, 0.24238217, 0.9999993), (-1, 1, -1), (-0.24238217, -0.24238217, 0.9999993), (-1, -1, -1)] + bool[] primvars:sharp_face = [1, 1, 1, 1, 1, 1] ( + interpolation = "uniform" + ) + texCoord2f[] primvars:st = [(0.625, 0.5), (0.875, 0.5), (0.875, 0.75), (0.625, 0.75), (0.375, 0.75), (0.625, 0.75), (0.625, 1), (0.375, 1), (0.375, 0), (0.625, 0), (0.625, 0.25), (0.375, 0.25), (0.125, 0.5), (0.375, 0.5), (0.375, 0.75), (0.125, 0.75), (0.375, 0.5), (0.625, 0.5), (0.625, 0.75), (0.375, 0.75), (0.375, 0.25), (0.625, 0.25), (0.625, 0.5), (0.375, 0.5)] ( + interpolation = "faceVarying" + ) + uniform token subdivisionScheme = "none" + } + } + + def Xform "Camera" + { + custom string userProperties:blender:object_name = "Camera" + float3 xformOp:rotateXYZ = (77.15932, -0.000010048663, 45.891956) + float3 xformOp:scale = (0.99999994, 1, 0.99999994) + double3 xformOp:translate = (7.904968738555908, -7.6511077880859375, 2.4478447437286377) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"] + + def Camera "Camera" + { + float2 clippingRange = (0.1, 100) + float focalLength = 0.5 + float horizontalAperture = 0.36 + token projection = "perspective" + custom string userProperties:blender:data_name = "Camera" + float verticalAperture = 0.2025 + } + } + + def Scope "_materials" + { + def Material "Material" + { + token outputs:surface.connect = + + def Shader "Principled_BSDF" + { + uniform token info:id = "UsdPreviewSurface" + float inputs:clearcoat = 0 + float inputs:clearcoatRoughness = 0.03 + color3f inputs:diffuseColor = (0.8, 0.8, 0.8) + float inputs:ior = 1.45 + float inputs:metallic = 0 + float inputs:opacity = 1 + float inputs:roughness = 0.5 + float inputs:specular = 0.5 + token outputs:surface + } + } + } +} + diff --git a/tests/files/usd/usd_dome_light_1_stageZ_poleY.usda b/tests/files/usd/usd_dome_light_1_stageZ_poleY.usda new file mode 100644 index 00000000000..06052e0052c --- /dev/null +++ b/tests/files/usd/usd_dome_light_1_stageZ_poleY.usda @@ -0,0 +1,124 @@ +#usda 1.0 +( + defaultPrim = "root" + doc = "Blender v4.5.0 Alpha" + metersPerUnit = 1 + upAxis = "Z" +) + +def Xform "root" ( + customData = { + dictionary Blender = { + bool generated = 1 + } + } +) +{ + def DomeLight_1 "env_light" + { + float inputs:intensity = 1 + asset inputs:texture:file = @./textures/test_single.png@ + float3 xformOp:rotateXYZ = (90, 0, 90) + uniform token[] xformOpOrder = ["xformOp:rotateXYZ"] + uniform token poleAxis = "Y" + } + + def Xform "Cube" + { + def Mesh "Cube" ( + active = true + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + uniform bool doubleSided = 1 + float3[] extent = [(-1, -1, -1), (1, 1, 1)] + int[] faceVertexCounts = [4, 4, 4, 4, 4, 4] + int[] faceVertexIndices = [0, 4, 6, 2, 3, 2, 6, 7, 7, 6, 4, 5, 5, 1, 3, 7, 1, 0, 2, 3, 5, 4, 0, 1] + rel material:binding = + normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1), (1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0)] ( + interpolation = "faceVarying" + ) + point3f[] points = [(1, 1, 1), (1, 1, -1), (1, -1, 1), (1, -1, -1), (-1, 1, 1), (-1, 1, -1), (-1, -1, 1), (-1, -1, -1)] + bool[] primvars:sharp_face = [1, 1, 1, 1, 1, 1] ( + interpolation = "uniform" + ) + texCoord2f[] primvars:st = [(0.625, 0.5), (0.875, 0.5), (0.875, 0.75), (0.625, 0.75), (0.375, 0.75), (0.625, 0.75), (0.625, 1), (0.375, 1), (0.375, 0), (0.625, 0), (0.625, 0.25), (0.375, 0.25), (0.125, 0.5), (0.375, 0.5), (0.375, 0.75), (0.125, 0.75), (0.375, 0.5), (0.625, 0.5), (0.625, 0.75), (0.375, 0.75), (0.375, 0.25), (0.625, 0.25), (0.625, 0.5), (0.375, 0.5)] ( + interpolation = "faceVarying" + ) + uniform token subdivisionScheme = "none" + } + } + + def Xform "Cube_001" + { + float3 xformOp:rotateXYZ = (0, -0, 0) + float3 xformOp:scale = (0.2904613, 0.2904613, 0.2904613) + double3 xformOp:translate = (0, 0, 1.6628384590148926) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"] + + def Mesh "Cube_001" ( + active = true + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + uniform bool doubleSided = 1 + float3[] extent = [(-1, -1, -1), (1, 1, 0.9999993)] + int[] faceVertexCounts = [4, 4, 4, 4, 4, 4] + int[] faceVertexIndices = [0, 4, 6, 2, 3, 2, 6, 7, 7, 6, 4, 5, 5, 1, 3, 7, 1, 0, 2, 3, 5, 4, 0, 1] + rel material:binding = + normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, -0.9351529, 0.3542444), (0, -0.9351529, 0.3542444), (0, -0.9351529, 0.3542444), (0, -0.9351529, 0.3542444), (-0.9351529, 0, 0.35424438), (-0.9351529, 0, 0.35424438), (-0.9351529, 0, 0.35424438), (-0.9351529, 0, 0.35424438), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0.9351529, 0, 0.35424438), (0.9351529, 0, 0.35424438), (0.9351529, 0, 0.35424438), (0.9351529, 0, 0.35424438), (0, 0.9351529, 0.3542444), (0, 0.9351529, 0.3542444), (0, 0.9351529, 0.3542444), (0, 0.9351529, 0.3542444)] ( + interpolation = "faceVarying" + ) + point3f[] points = [(0.24238217, 0.24238217, 0.9999993), (1, 1, -1), (0.24238217, -0.24238217, 0.9999993), (1, -1, -1), (-0.24238217, 0.24238217, 0.9999993), (-1, 1, -1), (-0.24238217, -0.24238217, 0.9999993), (-1, -1, -1)] + bool[] primvars:sharp_face = [1, 1, 1, 1, 1, 1] ( + interpolation = "uniform" + ) + texCoord2f[] primvars:st = [(0.625, 0.5), (0.875, 0.5), (0.875, 0.75), (0.625, 0.75), (0.375, 0.75), (0.625, 0.75), (0.625, 1), (0.375, 1), (0.375, 0), (0.625, 0), (0.625, 0.25), (0.375, 0.25), (0.125, 0.5), (0.375, 0.5), (0.375, 0.75), (0.125, 0.75), (0.375, 0.5), (0.625, 0.5), (0.625, 0.75), (0.375, 0.75), (0.375, 0.25), (0.625, 0.25), (0.625, 0.5), (0.375, 0.5)] ( + interpolation = "faceVarying" + ) + uniform token subdivisionScheme = "none" + } + } + + def Xform "Camera" + { + custom string userProperties:blender:object_name = "Camera" + float3 xformOp:rotateXYZ = (77.15932, -0.000010048663, 45.891956) + float3 xformOp:scale = (0.99999994, 1, 0.99999994) + double3 xformOp:translate = (7.904968738555908, -7.6511077880859375, 2.4478447437286377) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"] + + def Camera "Camera" + { + float2 clippingRange = (0.1, 100) + float focalLength = 0.5 + float horizontalAperture = 0.36 + token projection = "perspective" + custom string userProperties:blender:data_name = "Camera" + float verticalAperture = 0.2025 + } + } + + def Scope "_materials" + { + def Material "Material" + { + token outputs:surface.connect = + + def Shader "Principled_BSDF" + { + uniform token info:id = "UsdPreviewSurface" + float inputs:clearcoat = 0 + float inputs:clearcoatRoughness = 0.03 + color3f inputs:diffuseColor = (0.8, 0.8, 0.8) + float inputs:ior = 1.45 + float inputs:metallic = 0 + float inputs:opacity = 1 + float inputs:roughness = 0.5 + float inputs:specular = 0.5 + token outputs:surface + } + } + } +} + diff --git a/tests/files/usd/usd_dome_light_1_stageZ_poleZ.usda b/tests/files/usd/usd_dome_light_1_stageZ_poleZ.usda new file mode 100644 index 00000000000..438841b17de --- /dev/null +++ b/tests/files/usd/usd_dome_light_1_stageZ_poleZ.usda @@ -0,0 +1,124 @@ +#usda 1.0 +( + defaultPrim = "root" + doc = "Blender v4.5.0 Alpha" + metersPerUnit = 1 + upAxis = "Z" +) + +def Xform "root" ( + customData = { + dictionary Blender = { + bool generated = 1 + } + } +) +{ + def DomeLight_1 "env_light" + { + float inputs:intensity = 1 + asset inputs:texture:file = @./textures/test_single.png@ + float3 xformOp:rotateXYZ = (90, 0, 90) + uniform token[] xformOpOrder = ["xformOp:rotateXYZ"] + uniform token poleAxis = "Z" + } + + def Xform "Cube" + { + def Mesh "Cube" ( + active = true + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + uniform bool doubleSided = 1 + float3[] extent = [(-1, -1, -1), (1, 1, 1)] + int[] faceVertexCounts = [4, 4, 4, 4, 4, 4] + int[] faceVertexIndices = [0, 4, 6, 2, 3, 2, 6, 7, 7, 6, 4, 5, 5, 1, 3, 7, 1, 0, 2, 3, 5, 4, 0, 1] + rel material:binding = + normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1), (1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0)] ( + interpolation = "faceVarying" + ) + point3f[] points = [(1, 1, 1), (1, 1, -1), (1, -1, 1), (1, -1, -1), (-1, 1, 1), (-1, 1, -1), (-1, -1, 1), (-1, -1, -1)] + bool[] primvars:sharp_face = [1, 1, 1, 1, 1, 1] ( + interpolation = "uniform" + ) + texCoord2f[] primvars:st = [(0.625, 0.5), (0.875, 0.5), (0.875, 0.75), (0.625, 0.75), (0.375, 0.75), (0.625, 0.75), (0.625, 1), (0.375, 1), (0.375, 0), (0.625, 0), (0.625, 0.25), (0.375, 0.25), (0.125, 0.5), (0.375, 0.5), (0.375, 0.75), (0.125, 0.75), (0.375, 0.5), (0.625, 0.5), (0.625, 0.75), (0.375, 0.75), (0.375, 0.25), (0.625, 0.25), (0.625, 0.5), (0.375, 0.5)] ( + interpolation = "faceVarying" + ) + uniform token subdivisionScheme = "none" + } + } + + def Xform "Cube_001" + { + float3 xformOp:rotateXYZ = (0, -0, 0) + float3 xformOp:scale = (0.2904613, 0.2904613, 0.2904613) + double3 xformOp:translate = (0, 0, 1.6628384590148926) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"] + + def Mesh "Cube_001" ( + active = true + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + uniform bool doubleSided = 1 + float3[] extent = [(-1, -1, -1), (1, 1, 0.9999993)] + int[] faceVertexCounts = [4, 4, 4, 4, 4, 4] + int[] faceVertexIndices = [0, 4, 6, 2, 3, 2, 6, 7, 7, 6, 4, 5, 5, 1, 3, 7, 1, 0, 2, 3, 5, 4, 0, 1] + rel material:binding = + normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, -0.9351529, 0.3542444), (0, -0.9351529, 0.3542444), (0, -0.9351529, 0.3542444), (0, -0.9351529, 0.3542444), (-0.9351529, 0, 0.35424438), (-0.9351529, 0, 0.35424438), (-0.9351529, 0, 0.35424438), (-0.9351529, 0, 0.35424438), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0.9351529, 0, 0.35424438), (0.9351529, 0, 0.35424438), (0.9351529, 0, 0.35424438), (0.9351529, 0, 0.35424438), (0, 0.9351529, 0.3542444), (0, 0.9351529, 0.3542444), (0, 0.9351529, 0.3542444), (0, 0.9351529, 0.3542444)] ( + interpolation = "faceVarying" + ) + point3f[] points = [(0.24238217, 0.24238217, 0.9999993), (1, 1, -1), (0.24238217, -0.24238217, 0.9999993), (1, -1, -1), (-0.24238217, 0.24238217, 0.9999993), (-1, 1, -1), (-0.24238217, -0.24238217, 0.9999993), (-1, -1, -1)] + bool[] primvars:sharp_face = [1, 1, 1, 1, 1, 1] ( + interpolation = "uniform" + ) + texCoord2f[] primvars:st = [(0.625, 0.5), (0.875, 0.5), (0.875, 0.75), (0.625, 0.75), (0.375, 0.75), (0.625, 0.75), (0.625, 1), (0.375, 1), (0.375, 0), (0.625, 0), (0.625, 0.25), (0.375, 0.25), (0.125, 0.5), (0.375, 0.5), (0.375, 0.75), (0.125, 0.75), (0.375, 0.5), (0.625, 0.5), (0.625, 0.75), (0.375, 0.75), (0.375, 0.25), (0.625, 0.25), (0.625, 0.5), (0.375, 0.5)] ( + interpolation = "faceVarying" + ) + uniform token subdivisionScheme = "none" + } + } + + def Xform "Camera" + { + custom string userProperties:blender:object_name = "Camera" + float3 xformOp:rotateXYZ = (77.15932, -0.000010048663, 45.891956) + float3 xformOp:scale = (0.99999994, 1, 0.99999994) + double3 xformOp:translate = (7.904968738555908, -7.6511077880859375, 2.4478447437286377) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"] + + def Camera "Camera" + { + float2 clippingRange = (0.1, 100) + float focalLength = 0.5 + float horizontalAperture = 0.36 + token projection = "perspective" + custom string userProperties:blender:data_name = "Camera" + float verticalAperture = 0.2025 + } + } + + def Scope "_materials" + { + def Material "Material" + { + token outputs:surface.connect = + + def Shader "Principled_BSDF" + { + uniform token info:id = "UsdPreviewSurface" + float inputs:clearcoat = 0 + float inputs:clearcoatRoughness = 0.03 + color3f inputs:diffuseColor = (0.8, 0.8, 0.8) + float inputs:ior = 1.45 + float inputs:metallic = 0 + float inputs:opacity = 1 + float inputs:roughness = 0.5 + float inputs:specular = 0.5 + token outputs:surface + } + } + } +} + diff --git a/tests/python/bl_usd_import_test.py b/tests/python/bl_usd_import_test.py index e77149801fd..2e53a0c1aef 100644 --- a/tests/python/bl_usd_import_test.py +++ b/tests/python/bl_usd_import_test.py @@ -1204,6 +1204,29 @@ class USDImportTest(AbstractUSDTest): self.assertEqual(blender_light.shape, 'DISK') # We read as disk to mirror what USD supports self.assertAlmostEqual(blender_light.size, 4, 3) + def test_import_dome_lights(self): + """Test importing dome lights and verify their rotations.""" + + # Test files and their expected EnvironmentTexture Mapping rotation values + tests = [ + ("usd_dome_light_1_stageZ_poleY.usda", [0.0, 0.0, 0.0]), + ("usd_dome_light_1_stageZ_poleZ.usda", [0.0, -1.5708, 0.0]), + ("usd_dome_light_1_stageY_poleDefault.usda", [-1.5708, 0.0, 0.0]) + ] + + for test_name, expected_rot in tests: + bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend")) + + infile = str(self.testdir / test_name) + res = bpy.ops.wm.usd_import(filepath=infile) + self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {infile}") + + # Validate that the Mapping node on the World Material is set to the correct rotation + world = bpy.data.worlds["World"] + node = world.node_tree.nodes["Mapping"] + self.assertEqual( + self.round_vector(node.inputs[2].default_value), expected_rot, f"Incorrect rotation for {test_name}") + def check_attribute(self, blender_data, attribute_name, domain, data_type, elements_len): attr = blender_data.attributes[attribute_name] self.assertEqual(attr.domain, domain)