Blender V5.0
usd_light_convert.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2023 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
6
7#include "usd.hh"
8#include "usd_asset_utils.hh"
9#include "usd_private.hh"
10#include "usd_utils.hh"
12
13#include <pxr/base/gf/rotation.h>
14#include <pxr/base/gf/vec3f.h>
15#include <pxr/usd/usdGeom/metrics.h>
16#include <pxr/usd/usdGeom/tokens.h>
17#include <pxr/usd/usdGeom/xformCache.h>
18#include <pxr/usd/usdGeom/xformCommonAPI.h>
19#include <pxr/usd/usdLux/domeLight.h>
20#include <pxr/usd/usdLux/tokens.h>
21
22#include "BKE_image.hh"
23#include "BKE_library.hh"
24#include "BKE_main.hh"
25#include "BKE_node.hh"
27#include "BKE_node_runtime.hh"
29
30#include "BLI_fileops.h"
31#include "BLI_math_vector.h"
32#include "BLI_path_utils.hh"
33#include "BLI_span.hh"
34#include "BLI_string_ref.hh"
35#include "BLI_string_utils.hh"
36#include "BLI_utildefines.h"
37
38#include "DNA_image_types.h"
39#include "DNA_node_types.h"
40#include "DNA_scene_types.h"
41#include "DNA_world_types.h"
42
43#include <cmath>
44#include <cstdint>
45#include <string>
46
47#include "CLG_log.h"
48static CLG_LogRef LOG = {"io.usd"};
49
50namespace usdtokens {
51// Attribute values.
52static const pxr::TfToken pole_axis_z("Z", pxr::TfToken::Immortal);
53} // namespace usdtokens
54
55namespace {
56
57struct WorldNtreeSearchPayload {
59 pxr::UsdStageRefPtr stage;
60
61 WorldNtreeSearchPayload(const blender::io::usd::USDExportParams &in_params,
62 pxr::UsdStageRefPtr in_stage)
63 : params(in_params), stage(in_stage)
64 {
65 }
66};
67
68} // End anonymous namespace.
69
70namespace blender::io::usd {
71
76static Image *load_image(std::string tex_path, Main *bmain, const USDImportParams &params)
77{
78 /* Optionally copy the asset if it's inside a USDZ package. */
79 const bool import_textures = params.import_textures_mode != USD_TEX_IMPORT_NONE &&
80 should_import_asset(tex_path);
81
82 std::string imported_file_source_path = tex_path;
83
84 if (import_textures) {
85 /* If we are packing the imported textures, we first write them
86 * to a temporary directory. */
87 const char *textures_dir = params.import_textures_mode == USD_TEX_IMPORT_PACK ?
89 params.import_textures_dir;
90
91 const eUSDTexNameCollisionMode name_collision_mode = params.import_textures_mode ==
94 params.tex_name_collision_mode;
95
96 tex_path = import_asset(tex_path, textures_dir, name_collision_mode, nullptr);
97 }
98
99 Image *image = BKE_image_load_exists(bmain, tex_path.c_str());
100 if (!image) {
101 return nullptr;
102 }
103
104 if (import_textures && imported_file_source_path != tex_path) {
105 ensure_usd_source_path_prop(imported_file_source_path, &image->id);
106 }
107
108 if (import_textures && params.import_textures_mode == USD_TEX_IMPORT_PACK &&
110 {
111 BKE_image_packfiles(nullptr, image, ID_BLEND_PATH(bmain, &image->id));
113 BLI_delete(temp_textures_dir(), true, true);
114 }
115 }
116
117 return image;
118}
119
120/* Create a new node of type 'new_node_type' and connect it
121 * as an upstream source to 'dst_node' with the given sockets. */
122static bNode *append_node(bNode *dst_node,
123 int16_t new_node_type,
124 const StringRef out_sock,
125 const StringRef in_sock,
126 bNodeTree *ntree,
127 float offset)
128{
129 bNode *src_node = bke::node_add_static_node(nullptr, *ntree, new_node_type);
130 bke::node_add_link(*ntree,
131 *src_node,
132 *bke::node_find_socket(*src_node, SOCK_OUT, out_sock),
133 *dst_node,
134 *bke::node_find_socket(*dst_node, SOCK_IN, in_sock));
135
136 src_node->location[0] = dst_node->location[0] - offset;
137 src_node->location[1] = dst_node->location[1];
138
139 return src_node;
140}
141
143 const Scene *scene,
144 pxr::UsdStageRefPtr stage)
145{
146 if (!(stage && scene && scene->world)) {
147 return;
148 }
149
152
153 if (!(res.color_found || res.image)) {
154 /* No nodes to convert */
155 return;
156 }
157
158 std::string image_filepath;
159 if (res.image) {
160 /* Compute image filepath and export if needed. */
161 image_filepath = get_tex_image_asset_filepath(res.image, stage, params);
162 if (image_filepath.empty()) {
163 return;
164 }
165 if (params.export_textures) {
166 export_texture(res.image, stage, params.overwrite_textures);
167 }
168 }
169
170 /* Create USD dome light. */
171 pxr::SdfPath env_light_path = get_unique_path(stage, params.root_prim_path + "/env_light");
172 pxr::UsdLuxDomeLight dome_light = pxr::UsdLuxDomeLight::Define(stage, env_light_path);
173
174 if (res.image) {
175 /* Use existing image texture file. */
176 dome_light.CreateTextureFileAttr().Set(pxr::SdfAssetPath(image_filepath));
177
178 /* Set optional color multiplication. */
179 if (res.mult_found) {
180 pxr::GfVec3f color_val(res.color_mult[0], res.color_mult[1], res.color_mult[2]);
181 dome_light.CreateColorAttr().Set(color_val);
182 }
183
184 /* Set transform. */
185 pxr::GfVec3d angles = res.transform.DecomposeRotation(
186 pxr::GfVec3d::ZAxis(), pxr::GfVec3d::YAxis(), pxr::GfVec3d::XAxis());
187 pxr::GfVec3f rot_vec(angles[2], angles[1], angles[0]);
188 pxr::UsdGeomXformCommonAPI xform_api(dome_light);
189 xform_api.SetRotate(rot_vec, pxr::UsdGeomXformCommonAPI::RotationOrderXYZ);
190 }
191 else if (res.color_found) {
192 /* If no texture is found export a solid color texture as a stand-in so that Hydra
193 * renderers don't throw errors. */
194 dome_light.CreateIntensityAttr().Set(res.intensity);
195
196 std::string source_path = cache_image_color(res.color);
197 const std::string base_path = stage->GetRootLayer()->GetRealPath();
198
199 char file_path[FILE_MAX];
200 BLI_path_split_file_part(source_path.c_str(), file_path, FILE_MAX);
201 char dest_path[FILE_MAX];
202 BLI_path_split_dir_part(base_path.c_str(), dest_path, FILE_MAX);
203
204 BLI_path_append_dir(dest_path, FILE_MAX, "textures");
205 BLI_dir_create_recursive(dest_path);
206
207 BLI_path_append(dest_path, FILE_MAX, file_path);
208
209 if (BLI_copy(source_path.c_str(), dest_path) != 0) {
210 CLOG_WARN(&LOG, "USD Export: Couldn't write world color image to %s", dest_path);
211 }
212 else {
213 BLI_path_join(dest_path, FILE_MAX, ".", "textures", file_path);
214 BLI_string_replace_char(dest_path, '\\', '/');
215 dome_light.CreateTextureFileAttr().Set(pxr::SdfAssetPath(dest_path));
216 }
217 }
218}
219
220/* Import the dome light as a world material. */
221
223 Scene *scene,
224 Main *bmain,
225 const USDImportDomeLightData &dome_light_data,
226 const pxr::UsdPrim &prim,
227 const pxr::UsdTimeCode time)
228{
229 if (!(scene && scene->world && prim)) {
230 return;
231 }
232
233 if (!scene->world->nodetree) {
235 nullptr, &scene->world->id, "Shader Nodetree", "ShaderNodeTree");
236 }
237
238 bNodeTree *ntree = scene->world->nodetree;
239 bNode *output = nullptr;
240 bNode *bgshader = nullptr;
241
242 /* We never delete existing nodes, but we might disconnect them
243 * and move them out of the way. */
244
245 /* Look for the output and background shader nodes, which we will reuse. */
246 for (bNode *node : ntree->all_nodes()) {
247 if (node->type_legacy == SH_NODE_OUTPUT_WORLD) {
248 output = node;
249 }
250 else if (node->type_legacy == SH_NODE_BACKGROUND) {
251 bgshader = node;
252 }
253 else {
254 /* Move existing node out of the way. */
255 node->location[1] += 300;
256 }
257 }
258
259 /* Create the output and background shader nodes, if they don't exist. */
260 if (!output) {
262 output->location[0] = 300.0f;
263 output->location[1] = 300.0f;
264 }
265
266 if (!bgshader) {
267 bgshader = append_node(output, SH_NODE_BACKGROUND, "Background", "Surface", ntree, 200);
268
269 /* Set the default background color. */
270 bNodeSocket *color_sock = bke::node_find_socket(*bgshader, SOCK_IN, "Color");
271 copy_v3_v3(((bNodeSocketValueRGBA *)color_sock->default_value)->value, &scene->world->horr);
272 }
273
274 /* Make sure the first input to the shader node is disconnected. */
275 bNodeSocket *shader_input = bke::node_find_socket(*bgshader, SOCK_IN, "Color");
276
277 if (shader_input && shader_input->link) {
278 bke::node_remove_link(ntree, *shader_input->link);
279 }
280
281 /* Set the background shader intensity. */
282 float intensity = dome_light_data.intensity * params.light_intensity_scale;
283
284 bNodeSocket *strength_sock = bke::node_find_socket(*bgshader, SOCK_IN, "Strength");
285 ((bNodeSocketValueFloat *)strength_sock->default_value)->value = intensity;
286
287 if (!dome_light_data.has_tex) {
288 /* No texture file is authored on the dome light. Set the color, if it was authored,
289 * and return early. */
290 if (dome_light_data.has_color) {
291 bNodeSocket *color_sock = bke::node_find_socket(*bgshader, SOCK_IN, "Color");
292 copy_v3_v3(((bNodeSocketValueRGBA *)color_sock->default_value)->value,
293 dome_light_data.color.data());
294 }
295
298
299 return;
300 }
301
302 /* If the light has authored color, create a color multiply node for the environment
303 * texture output. */
304 bNode *mult = nullptr;
305
306 if (dome_light_data.has_color) {
307 mult = append_node(bgshader, SH_NODE_VECTOR_MATH, "Vector", "Color", ntree, 200);
309
310 /* Set the color in the vector math node's second socket. */
311 bNodeSocket *vec_sock = bke::node_find_socket(*mult, SOCK_IN, "Vector");
312 if (vec_sock) {
313 vec_sock = vec_sock->next;
314 }
315
316 if (vec_sock) {
317 copy_v3_v3(((bNodeSocketValueVector *)vec_sock->default_value)->value,
318 dome_light_data.color.data());
319 }
320 else {
321 CLOG_WARN(&LOG, "Couldn't find vector multiply second vector socket");
322 }
323 }
324
325 bNode *tex = nullptr;
326
327 /* Append an environment texture node to the mult node, if it was created, or directly to
328 * the background shader. */
329 if (mult) {
330 tex = append_node(mult, SH_NODE_TEX_ENVIRONMENT, "Color", "Vector", ntree, 400);
331 }
332 else {
333 tex = append_node(bgshader, SH_NODE_TEX_ENVIRONMENT, "Color", "Color", ntree, 400);
334 }
335
336 bNode *mapping = append_node(tex, SH_NODE_MAPPING, "Vector", "Vector", ntree, 200);
337
338 append_node(mapping, SH_NODE_TEX_COORD, "Generated", "Vector", ntree, 200);
339
340 /* Load the texture image. */
341 const std::string &resolved_path = dome_light_data.tex_path.GetResolvedPath();
342 if (resolved_path.empty()) {
343 CLOG_WARN(&LOG,
344 "Couldn't get resolved path for asset %s",
345 dome_light_data.tex_path.GetAssetPath().c_str());
346 return;
347 }
348
349 Image *image = load_image(resolved_path, bmain, params);
350 if (!image) {
351 CLOG_WARN(&LOG, "Couldn't load image file %s", resolved_path.c_str());
352 return;
353 }
354
355 tex->id = &image->id;
356
357 /* Set the transform. */
358 pxr::UsdGeomXformCache xf_cache(time);
359 pxr::GfMatrix4d xf = xf_cache.GetLocalToWorldTransform(prim);
360
361 pxr::UsdStageRefPtr stage = prim.GetStage();
362
363 if (!stage) {
364 CLOG_WARN(&LOG, "Couldn't get stage for dome light %s", prim.GetPath().GetText());
365 return;
366 }
367
368 /* Note: This logic tries to produce identical results to `usdview` as of USD 25.05.
369 * However, `usdview` seems to handle Y-Up stages differently; some scenes match while others
370 * do not unless we keep the second conditional below (+90 on x-axis). */
371 const pxr::TfToken stage_up = pxr::UsdGeomGetStageUpAxis(stage);
372 const bool needs_stage_z_adjust = stage_up == pxr::UsdGeomTokens->z &&
373 ELEM(dome_light_data.pole_axis,
374 pxr::UsdLuxTokens->Z,
375 pxr::UsdLuxTokens->scene);
376 const bool needs_stage_y_adjust = stage_up == pxr::UsdGeomTokens->y &&
377 ELEM(dome_light_data.pole_axis, pxr::UsdLuxTokens->Z);
378 if (needs_stage_z_adjust || needs_stage_y_adjust) {
379 xf *= pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(0.0, 1.0, 0.0), 90.0));
380 }
381 else if (stage_up == pxr::UsdGeomTokens->y) {
382 /* Convert from Y-up to Z-up with a 90 degree rotation about the X-axis. */
383 xf *= pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(1.0, 0.0, 0.0), 90.0));
384 }
385
386 /* Rotate into Blender's frame of reference. */
387 xf = pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(0.0, 0.0, 1.0), -90.0)) *
388 pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(1.0, 0.0, 0.0), -90.0)) * xf;
389
390 pxr::GfVec3d angles = xf.DecomposeRotation(
391 pxr::GfVec3d::XAxis(), pxr::GfVec3d::YAxis(), pxr::GfVec3d::ZAxis());
392 pxr::GfVec3f rot_vec(-angles[0], -angles[1], -angles[2]);
393
394 /* Convert degrees to radians. */
395 rot_vec *= M_PI / 180.0f;
396
397 if (bNodeSocket *socket = bke::node_find_socket(*mapping, SOCK_IN, "Rotation")) {
398 bNodeSocketValueVector *rot_value = static_cast<bNodeSocketValueVector *>(
399 socket->default_value);
400 copy_v3_v3(rot_value->value, rot_vec.data());
401 }
402
406}
407
408static bool node_search(bNode *fromnode, bNode * /*tonode*/, void *userdata, bool /*reversed*/)
409{
410 if (!(userdata && fromnode)) {
411 return true;
412 }
413
414 WorldToDomeLight &res = *static_cast<WorldToDomeLight *>(userdata);
415
416 if (!res.color_found && fromnode->type_legacy == SH_NODE_BACKGROUND) {
417 /* Get light color and intensity */
418 const bNodeSocketValueRGBA *color_data = bke::node_find_socket(*fromnode, SOCK_IN, "Color")
419 ->default_value_typed<bNodeSocketValueRGBA>();
420 const bNodeSocketValueFloat *strength_data =
421 bke::node_find_socket(*fromnode, SOCK_IN, "Strength")
422 ->default_value_typed<bNodeSocketValueFloat>();
423
424 res.color_found = true;
425 res.intensity = strength_data->value;
426 res.color[0] = color_data->value[0];
427 res.color[1] = color_data->value[1];
428 res.color[2] = color_data->value[2];
429 res.color[3] = 1.0f;
430 }
431 else if (!res.image && fromnode->type_legacy == SH_NODE_TEX_ENVIRONMENT) {
432 NodeTexImage *tex = static_cast<NodeTexImage *>(fromnode->storage);
433 res.image = reinterpret_cast<Image *>(fromnode->id);
434 res.iuser = &tex->iuser;
435 }
436 else if (!res.image && !res.mult_found && fromnode->type_legacy == SH_NODE_VECTOR_MATH) {
437 if (fromnode->custom1 == NODE_VECTOR_MATH_MULTIPLY) {
438 res.mult_found = true;
439
440 bNodeSocket *vec_sock = bke::node_find_socket(*fromnode, SOCK_IN, "Vector");
441 if (vec_sock) {
442 vec_sock = vec_sock->next;
443 }
444
445 if (vec_sock) {
446 copy_v3_v3(res.color_mult, ((bNodeSocketValueVector *)vec_sock->default_value)->value);
447 }
448 }
449 }
450 else if (res.image && fromnode->type_legacy == SH_NODE_MAPPING) {
451 if (bNodeSocket *socket = bke::node_find_socket(*fromnode, SOCK_IN, "Rotation")) {
452 const bNodeSocketValueVector *rot_value = static_cast<bNodeSocketValueVector *>(
453 socket->default_value);
454 /* Convert radians to degrees. */
455 pxr::GfVec3f rot(rot_value->value);
456 mul_v3_fl(rot.data(), 180.0f / M_PI);
457 res.transform =
458 pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(1.0, 0.0, 0.0), 90.0)) *
459 pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(0.0, 0.0, 1.0), 90.0)) *
460 pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(0.0, 0.0, 1.0), -rot[2])) *
461 pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(0.0, 1.0, 0.0), -rot[1])) *
462 pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(1.0, 0.0, 0.0), -rot[0]));
463 }
464 }
465 return true;
466}
467
469{
470 /* Find the world output. */
471 scene->world->nodetree->ensure_topology_cache();
472 const Span<const bNode *> bsdf_nodes = scene->world->nodetree->nodes_by_type(
473 "ShaderNodeOutputWorld");
474
475 for (const bNode *node : bsdf_nodes) {
476 if (node->flag & NODE_DO_OUTPUT) {
477 bke::node_chain_iterator(scene->world->nodetree, node, node_search, &res, true);
478 break;
479 }
480 }
481}
482
483} // namespace blender::io::usd
void BKE_image_packfiles(ReportList *reports, Image *ima, const char *basepath)
Image * BKE_image_load_exists(Main *bmain, const char *filepath, bool *r_exists=nullptr)
bool BKE_image_has_packedfile(const Image *image)
#define SH_NODE_OUTPUT_WORLD
#define SH_NODE_TEX_COORD
#define SH_NODE_VECTOR_MATH
#define SH_NODE_TEX_ENVIRONMENT
#define SH_NODE_BACKGROUND
#define SH_NODE_MAPPING
void BKE_ntree_update_after_single_tree_change(Main &bmain, bNodeTree &modified_tree, const NodeTreeUpdateExtraParams &params={})
File and directory operations.
int BLI_copy(const char *path_src, const char *path_dst) ATTR_NONNULL()
bool BLI_dir_create_recursive(const char *dirname) ATTR_NONNULL()
Definition fileops_c.cc:414
int BLI_delete(const char *path, bool dir, bool recursive) ATTR_NONNULL()
bool BLI_is_dir(const char *path) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL()
Definition storage.cc:443
#define M_PI
MINLINE void mul_v3_fl(float r[3], float f)
MINLINE void copy_v3_v3(float r[3], const float a[3])
size_t BLI_path_append(char *__restrict dst, size_t dst_maxncpy, const char *__restrict file) ATTR_NONNULL(1
#define FILE_MAX
#define BLI_path_join(...)
void void void BLI_path_split_file_part(const char *filepath, char *file, size_t file_maxncpy) ATTR_NONNULL(1
void void BLI_path_split_dir_part(const char *filepath, char *dir, size_t dir_maxncpy) ATTR_NONNULL(1
size_t size_t BLI_path_append_dir(char *__restrict dst, size_t dst_maxncpy, const char *__restrict dir) ATTR_NONNULL(1
void BLI_string_replace_char(char *str, char src, char dst) ATTR_NONNULL(1)
#define ELEM(...)
#define CLOG_WARN(clg_ref,...)
Definition CLG_log.h:189
void DEG_id_tag_update(ID *id, unsigned int flags)
@ ID_RECALC_NTREE_OUTPUT
Definition DNA_ID.h:1155
#define ID_BLEND_PATH(_bmain, _id)
Definition DNA_ID.h:685
@ NODE_VECTOR_MATH_MULTIPLY
@ NODE_DO_OUTPUT
@ SOCK_OUT
@ SOCK_IN
SIMD_FORCE_INLINE void mult(const btTransform &t1, const btTransform &t2)
Set the current transform as the value of the product of two transforms.
Definition btTransform.h:76
#define rot(x, k)
#define output
uiWidgetBaseParameters params[MAX_WIDGET_BASE_BATCH]
#define LOG(level)
Definition log.h:97
bNodeTree * node_tree_add_tree_embedded(Main *bmain, ID *owner_id, StringRefNull name, StringRefNull idname)
Definition node.cc:4098
bNodeSocket * node_find_socket(bNode &node, eNodeSocketInOut in_out, StringRef identifier)
Definition node.cc:2532
void node_remove_link(bNodeTree *ntree, bNodeLink &link)
Definition node.cc:3847
void node_chain_iterator(const bNodeTree *ntree, const bNode *node_start, bool(*callback)(bNode *, bNode *, void *, const bool), void *userdata, bool reversed)
Definition node.cc:3340
bNode * node_add_static_node(const bContext *C, bNodeTree &ntree, int type)
Definition node.cc:3500
bNodeLink & node_add_link(bNodeTree &ntree, bNode &fromnode, bNodeSocket &fromsock, bNode &tonode, bNodeSocket &tosock)
Definition node.cc:3810
void node_set_active(bNodeTree &ntree, bNode &node)
Definition node.cc:4724
void world_material_to_dome_light(const USDExportParams &params, const Scene *scene, pxr::UsdStageRefPtr stage)
const char * temp_textures_dir()
bool should_import_asset(const std::string &path)
static Image * load_image(std::string tex_path, Main *bmain, const USDImportParams &params)
pxr::SdfPath get_unique_path(pxr::UsdStageRefPtr stage, const std::string &path)
Definition usd_utils.cc:61
static void export_texture(const USDExporterContext &usd_export_context, bNode *node)
@ USD_TEX_IMPORT_NONE
Definition usd.hh:65
@ USD_TEX_IMPORT_PACK
Definition usd.hh:66
void dome_light_to_world_material(const USDImportParams &params, Scene *scene, Main *bmain, const USDImportDomeLightData &dome_light_data, const pxr::UsdPrim &prim, const pxr::UsdTimeCode time)
std::string cache_image_color(const float color[4])
static bool node_search(bNode *fromnode, bNode *, void *userdata, bool)
eUSDTexNameCollisionMode
Definition usd.hh:74
@ USD_TEX_NAME_COLLISION_OVERWRITE
Definition usd.hh:76
static std::string get_tex_image_asset_filepath(const USDExporterContext &usd_export_context, bNode *node)
void ensure_usd_source_path_prop(const std::string &path, ID *id)
static bNode * append_node(bNode *dst_node, int16_t new_node_type, const StringRef out_sock, const StringRef in_sock, bNodeTree *ntree, float offset)
std::string import_asset(const std::string &src, const char *import_dir, eUSDTexNameCollisionMode name_collision_mode, ReportList *reports)
static const pxr::TfToken pole_axis_z("Z", pxr::TfToken::Immortal)
struct World * world
struct bNodeTree * nodetree
float horr
struct bNodeLink * link
struct bNodeSocket * next
void * default_value
float location[2]
int16_t custom1
struct ID * id
int16_t type_legacy
void * storage