1 /* SPDX-FileCopyrightText: 2019 Blender Authors
3 * SPDX-License-Identifier: GPL-2.0-or-later */
7 #include "IO_subdiv_disabler.hh"
9 #include "usd_hierarchy_iterator.hh"
10 #include "usd_hook.hh"
11 #include "usd_instancing_utils.hh"
12 #include "usd_light_convert.hh"
13 #include "usd_private.hh"
15 #include <pxr/base/tf/token.h>
17 #include <pxr/usd/sdf/assetPath.h>
18 #include <pxr/usd/usd/primRange.h>
19 #include <pxr/usd/usd/stage.h>
20 #include <pxr/usd/usdGeom/metrics.h>
21 #include <pxr/usd/usdGeom/tokens.h>
22 #include <pxr/usd/usdGeom/xform.h>
23 #include <pxr/usd/usdGeom/xformCommonAPI.h>
24 #include <pxr/usd/usdUtils/usdzPackage.h>
26 #include "MEM_guardedalloc.h"
28 #include "DEG_depsgraph.hh"
29 #include "DEG_depsgraph_build.hh"
30 #include "DEG_depsgraph_query.hh"
32 #include "DNA_collection_types.h"
33 #include "DNA_scene_types.h"
35 #include "BKE_appdir.hh"
36 #include "BKE_blender_version.h"
37 #include "BKE_context.hh"
38 #include "BKE_global.hh"
39 #include "BKE_image.hh"
40 #include "BKE_image_save.hh"
41 #include "BKE_lib_id.hh"
42 #include "BKE_report.hh"
43 #include "BKE_scene.hh"
45 #include "BLI_fileops.h"
46 #include "BLI_math_matrix.h"
47 #include "BLI_math_rotation.h"
48 #include "BLI_math_vector.h"
49 #include "BLI_path_utils.hh"
50 #include "BLI_string.h"
51 #include "BLI_timeit.hh"
53 #include <IMB_imbuf.hh>
54 #include <IMB_imbuf_types.hh>
57 #include "WM_types.hh"
60 static CLG_LogRef LOG
= {"io.usd"};
62 namespace blender::io::usd
{
64 struct ExportJobData
{
70 /** Unarchived_filepath is used for USDA/USDC/USD export. */
71 char unarchived_filepath
[FILE_MAX
];
72 char usdz_filepath
[FILE_MAX
];
73 USDExportParams params
;
76 timeit::TimePoint start_time
;
78 bool targets_usdz() const
80 return usdz_filepath
[0] != '\0';
83 const char *export_filepath() const
88 return unarchived_filepath
;
92 /* Returns true if the given prim path is valid, per
93 * the requirements of the prim path manipulation logic
94 * of the exporter. Also returns true if the path is
95 * the empty string. Returns false otherwise. */
96 static bool prim_path_valid(const char *path
)
100 if (path
[0] == '\0') {
101 /* Empty paths are ignored in the code,
102 * so they can be passed through. */
106 /* Check path syntax. */
108 if (!pxr::SdfPath::IsValidPathString(path
, &errMsg
)) {
109 WM_reportf(RPT_ERROR
, "USD Export: invalid path string '%s': %s", path
, errMsg
.c_str());
113 /* Verify that an absolute prim path can be constructed
114 * from this path string. */
116 pxr::SdfPath
sdf_path(path
);
117 if (!sdf_path
.IsAbsolutePath()) {
118 WM_reportf(RPT_ERROR
, "USD Export: path '%s' is not an absolute path", path
);
122 if (!sdf_path
.IsPrimPath()) {
123 WM_reportf(RPT_ERROR
, "USD Export: path string '%s' is not a prim path", path
);
131 * Perform validation of export parameter settings.
132 * \return true if the parameters are valid; returns false otherwise.
134 * \warning Do not call from worker thread, only from main thread (i.e. before starting the wmJob).
136 static bool export_params_valid(const USDExportParams
¶ms
)
140 if (!prim_path_valid(params
.root_prim_path
)) {
148 * Create the root Xform primitive, if the Root Prim path has been set
149 * in the export options. In the future, this function can be extended
150 * to author transforms and additional schema data (e.g., model Kind)
153 static void ensure_root_prim(pxr::UsdStageRefPtr stage
, const USDExportParams
¶ms
)
155 if (params
.root_prim_path
[0] == '\0') {
159 pxr::UsdGeomXform root_xf
= pxr::UsdGeomXform::Define(stage
,
160 pxr::SdfPath(params
.root_prim_path
));
166 pxr::UsdGeomXformCommonAPI
xf_api(root_xf
.GetPrim());
172 if (params
.convert_scene_units
) {
173 xf_api
.SetScale(pxr::GfVec3f(float(1.0 / get_meters_per_unit(params
))));
176 if (params
.convert_orientation
) {
178 mat3_from_axis_conversion(IO_AXIS_Y
, IO_AXIS_Z
, params
.forward_axis
, params
.up_axis
, mrot
);
182 mat3_to_eul(eul
, mrot
);
184 /* Convert radians to degrees. */
185 mul_v3_fl(eul
, 180.0f
/ M_PI
);
187 xf_api
.SetRotate(pxr::GfVec3f(eul
[0], eul
[1], eul
[2]));
190 for (const auto &path
: pxr::SdfPath(params
.root_prim_path
).GetPrefixes()) {
191 auto xform
= pxr::UsdGeomXform::Define(stage
, path
);
192 /* Tag generated primitives to allow filtering on import. */
193 xform
.GetPrim().SetCustomDataByKey(pxr::TfToken("Blender:generated"), pxr::VtValue(true));
197 static void report_job_duration(const ExportJobData
*data
)
199 timeit::Nanoseconds duration
= timeit::Clock::now() - data
->start_time
;
200 const char *export_filepath
= data
->export_filepath();
201 fmt::print("USD export of '{}' took ", export_filepath
);
202 timeit::print_duration(duration
);
206 static void process_usdz_textures(const ExportJobData
*data
, const char *path
)
208 const eUSDZTextureDownscaleSize enum_value
= data
->params
.usdz_downscale_size
;
209 if (enum_value
== USD_TEXTURE_SIZE_KEEP
) {
213 const int image_size
= (enum_value
== USD_TEXTURE_SIZE_CUSTOM
) ?
214 data
->params
.usdz_downscale_custom_size
:
217 char texture_path
[FILE_MAX
];
218 STRNCPY(texture_path
, path
);
219 BLI_path_append(texture_path
, FILE_MAX
, "textures");
220 BLI_path_slash_ensure(texture_path
, sizeof(texture_path
));
223 uint num_files
= BLI_filelist_dir_contents(texture_path
, &entries
);
225 for (int index
= 0; index
< num_files
; index
++) {
226 /* We can skip checking extensions as this folder is only created
227 * when we're doing a USDZ export. */
228 if (!BLI_is_dir(entries
[index
].path
)) {
229 Image
*im
= BKE_image_load(data
->bmain
, entries
[index
].path
);
231 CLOG_WARN(&LOG
, "Unable to open file for downscaling: %s", entries
[index
].path
);
236 BKE_image_get_size(im
, nullptr, &width
, &height
);
237 const int longest
= width
>= height
? width
: height
;
238 const float scale
= 1.0 / (float(longest
) / float(image_size
));
240 if (longest
> image_size
) {
241 const int width_adjusted
= float(width
) * scale
;
242 const int height_adjusted
= float(height
) * scale
;
243 BKE_image_scale(im
, width_adjusted
, height_adjusted
, nullptr);
245 ImageSaveOptions opts
;
247 if (BKE_image_save_options_init(
248 &opts
, data
->bmain
, data
->scene
, im
, nullptr, false, false))
250 bool result
= BKE_image_save(nullptr, data
->bmain
, im
, nullptr, &opts
);
253 "Unable to resave '%s' (new size: %dx%d)",
261 "Downscaled '%s' to %dx%d",
268 BKE_image_save_options_free(&opts
);
271 /* Make sure to free the image so it doesn't stick
272 * around in the library of the open file. */
273 BKE_id_free(data
->bmain
, (void *)im
);
277 BLI_filelist_free(entries
, num_files
);
281 * For usdz export, we must first create a usd/a/c file and then covert it to usdz. In Blender's
282 * case, we first create a usdc file in Blender's temporary working directory, and store the path
283 * to the usdc file in `unarchived_filepath`. This function then does the conversion of that usdc
286 * \return true when the conversion from usdc to usdz is successful.
288 static bool perform_usdz_conversion(const ExportJobData
*data
)
290 char usdc_temp_dir
[FILE_MAX
], usdc_file
[FILE_MAX
];
291 BLI_path_split_dir_file(data
->unarchived_filepath
,
293 sizeof(usdc_temp_dir
),
297 char usdz_file
[FILE_MAX
];
298 BLI_path_split_file_part(data
->usdz_filepath
, usdz_file
, FILE_MAX
);
300 char original_working_dir_buff
[FILE_MAX
];
301 const char *original_working_dir
= BLI_current_working_dir(original_working_dir_buff
,
302 sizeof(original_working_dir_buff
));
303 /* Buffer is expected to be returned by #BLI_current_working_dir, although in theory other
304 * returns are possible on some platforms, this is not handled by this code. */
305 BLI_assert(original_working_dir
== original_working_dir_buff
);
307 BLI_change_working_dir(usdc_temp_dir
);
309 process_usdz_textures(data
, usdc_temp_dir
);
311 pxr::UsdUtilsCreateNewUsdzPackage(pxr::SdfAssetPath(usdc_file
), usdz_file
);
312 BLI_change_working_dir(original_working_dir
);
314 char usdz_temp_full_path
[FILE_MAX
];
315 BLI_path_join(usdz_temp_full_path
, FILE_MAX
, usdc_temp_dir
, usdz_file
);
318 if (BLI_exists(data
->usdz_filepath
)) {
319 result
= BLI_delete(data
->usdz_filepath
, false, false);
321 BKE_reportf(data
->params
.worker_status
->reports
,
323 "USD Export: Unable to delete existing usdz file %s",
324 data
->usdz_filepath
);
328 result
= BLI_path_move(usdz_temp_full_path
, data
->usdz_filepath
);
330 BKE_reportf(data
->params
.worker_status
->reports
,
332 "USD Export: Couldn't move new usdz file from temporary location %s to %s",
334 data
->usdz_filepath
);
341 std::string
image_cache_file_path()
343 char dir_path
[FILE_MAX
];
344 BLI_path_join(dir_path
, sizeof(dir_path
), BKE_tempdir_session(), "usd", "image_cache");
348 std::string
get_image_cache_file(const std::string
&file_name
, bool mkdir
)
350 std::string dir_path
= image_cache_file_path();
352 BLI_dir_create_recursive(dir_path
.c_str());
355 char file_path
[FILE_MAX
];
356 BLI_path_join(file_path
, sizeof(file_path
), dir_path
.c_str(), file_name
.c_str());
360 std::string
cache_image_color(const float color
[4])
364 "color_%02d%02d%02d.hdr",
367 int(color
[2] * 255));
368 std::string file_path
= get_image_cache_file(name
);
369 if (BLI_exists(file_path
.c_str())) {
373 ImBuf
*ibuf
= IMB_allocImBuf(4, 4, 32, IB_rectfloat
);
374 IMB_rectfill(ibuf
, color
);
375 ibuf
->ftype
= IMB_FTYPE_RADHDR
;
377 if (IMB_saveiff(ibuf
, file_path
.c_str(), IB_rectfloat
)) {
378 CLOG_INFO(&LOG
, 1, "%s", file_path
.c_str());
381 CLOG_ERROR(&LOG
, "Can't save %s", file_path
.c_str());
389 pxr::UsdStageRefPtr
export_to_stage(const USDExportParams
¶ms
,
390 Depsgraph
*depsgraph
,
391 const char *filepath
)
393 pxr::UsdStageRefPtr usd_stage
= pxr::UsdStage::CreateNew(filepath
);
398 wmJobWorkerStatus
*worker_status
= params
.worker_status
;
399 Scene
*scene
= DEG_get_input_scene(depsgraph
);
400 Main
*bmain
= DEG_get_bmain(depsgraph
);
402 SubdivModifierDisabler
mod_disabler(depsgraph
);
404 /* If we want to set the subdiv scheme, then we need to the export the mesh
405 * without the subdiv modifier applied. */
406 if (ELEM(params
.export_subdiv
, USD_SUBDIV_BEST_MATCH
, USD_SUBDIV_IGNORE
)) {
407 mod_disabler
.disable_modifiers();
408 BKE_scene_graph_update_tagged(depsgraph
, bmain
);
411 /* This whole `export_to_stage` function is assumed to cover about 80% of the whole export
412 * process, from 0.1f to 0.9f. */
413 worker_status
->progress
= 0.10f
;
414 worker_status
->do_update
= true;
416 usd_stage
->SetMetadata(pxr::UsdGeomTokens
->metersPerUnit
, double(scene
->unit
.scale_length
));
417 usd_stage
->GetRootLayer()->SetDocumentation(std::string("Blender v") +
418 BKE_blender_version_string());
420 /* Set up the stage for animated data. */
421 if (params
.export_animation
) {
422 usd_stage
->SetTimeCodesPerSecond(FPS
);
423 usd_stage
->SetStartTimeCode(scene
->r
.sfra
);
424 usd_stage
->SetEndTimeCode(scene
->r
.efra
);
427 /* For restoring the current frame after exporting animation is done. */
428 const int orig_frame
= scene
->r
.cfra
;
430 /* Ensure Python types for invoking hooks are registered. */
431 register_hook_converters();
433 pxr::VtValue upAxis
= pxr::VtValue(pxr::UsdGeomTokens
->z
);
434 if (params
.convert_orientation
) {
435 if (params
.up_axis
== IO_AXIS_X
) {
436 upAxis
= pxr::VtValue(pxr::UsdGeomTokens
->x
);
438 else if (params
.up_axis
== IO_AXIS_Y
) {
439 upAxis
= pxr::VtValue(pxr::UsdGeomTokens
->y
);
443 usd_stage
->SetMetadata(pxr::UsdGeomTokens
->upAxis
, upAxis
);
445 const double meters_per_unit
= get_meters_per_unit(params
);
446 pxr::UsdGeomSetStageMetersPerUnit(usd_stage
, meters_per_unit
);
448 ensure_root_prim(usd_stage
, params
);
450 USDHierarchyIterator
iter(bmain
, depsgraph
, usd_stage
, params
);
452 worker_status
->progress
= 0.11f
;
453 worker_status
->do_update
= true;
455 if (params
.export_animation
) {
456 /* Writing the animated frames is not 100% of the work, here it's assumed to be 75% of it. */
457 float progress_per_frame
= 0.75f
/ std::max(1, (scene
->r
.efra
- scene
->r
.sfra
+ 1));
459 for (float frame
= scene
->r
.sfra
; frame
<= scene
->r
.efra
; frame
++) {
460 if (G
.is_break
|| worker_status
->stop
) {
464 /* Update the scene for the next frame to render. */
465 scene
->r
.cfra
= int(frame
);
466 scene
->r
.subframe
= frame
- scene
->r
.cfra
;
467 BKE_scene_graph_update_for_newframe(depsgraph
);
469 iter
.set_export_frame(frame
);
470 iter
.iterate_and_write();
472 worker_status
->progress
+= progress_per_frame
;
473 worker_status
->do_update
= true;
477 /* If we're not animating, a single iteration over all objects is enough. */
478 iter
.iterate_and_write();
481 worker_status
->progress
= 0.86f
;
482 worker_status
->do_update
= true;
484 iter
.release_writers();
486 if (params
.export_shapekeys
|| params
.export_armatures
) {
487 iter
.process_usd_skel();
490 /* Creating dome lights should be called after writers have
491 * completed, to avoid a name collision when creating the light
493 if (params
.convert_world_material
) {
494 world_material_to_dome_light(params
, scene
, usd_stage
);
497 /* Set the default prim if it doesn't exist */
498 if (!usd_stage
->GetDefaultPrim()) {
499 /* Use TraverseAll since it's guaranteed to be depth first and will get the first top level
500 * prim, and is less verbose than getting the PseudoRoot + iterating its children. */
501 for (auto prim
: usd_stage
->TraverseAll()) {
502 usd_stage
->SetDefaultPrim(prim
);
507 if (params
.use_instancing
) {
508 process_scene_graph_instances(params
, usd_stage
);
511 call_export_hooks(usd_stage
, depsgraph
, params
.worker_status
->reports
);
513 worker_status
->progress
= 0.88f
;
514 worker_status
->do_update
= true;
516 /* Finish up by going back to the keyframe that was current before we started. */
517 if (scene
->r
.cfra
!= orig_frame
) {
518 scene
->r
.cfra
= orig_frame
;
519 BKE_scene_graph_update_for_newframe(depsgraph
);
522 worker_status
->progress
= 0.9f
;
523 worker_status
->do_update
= true;
528 static void export_startjob(void *customdata
, wmJobWorkerStatus
*worker_status
)
530 ExportJobData
*data
= static_cast<ExportJobData
*>(customdata
);
531 data
->export_ok
= false;
532 data
->start_time
= timeit::Clock::now();
534 G
.is_rendering
= true;
536 WM_set_locked_interface(data
->wm
, true);
540 worker_status
->progress
= 0.01f
;
541 worker_status
->do_update
= true;
543 /* Evaluate the depsgraph for exporting.
545 * Note that, unlike with its building, this is expected to be safe to perform from worker
546 * thread, since UI is locked during export, so there should not be any more changes in the Main
547 * original data concurrently done from the main thread at this point. All necessary (deferred)
548 * changes are expected to have been triggered and processed during depsgraph building in
550 BKE_scene_graph_update_tagged(data
->depsgraph
, data
->bmain
);
552 worker_status
->progress
= 0.1f
;
553 worker_status
->do_update
= true;
554 data
->params
.worker_status
= worker_status
;
556 pxr::UsdStageRefPtr usd_stage
= export_to_stage(
557 data
->params
, data
->depsgraph
, data
->unarchived_filepath
);
559 /* This happens when the USD JSON files cannot be found. When that happens,
560 * the USD library doesn't know it has the functionality to write USDA and
561 * USDC files, and creating a new UsdStage fails. */
562 BKE_reportf(worker_status
->reports
,
564 "USD Export: unable to find suitable USD plugin to write %s",
565 data
->unarchived_filepath
);
569 usd_stage
->GetRootLayer()->Save();
571 data
->export_ok
= true;
572 worker_status
->progress
= 1.0f
;
573 worker_status
->do_update
= true;
576 static void export_endjob_usdz_cleanup(const ExportJobData
*data
)
578 if (!BLI_exists(data
->unarchived_filepath
)) {
583 BLI_path_split_dir_part(data
->unarchived_filepath
, dir
, FILE_MAX
);
585 char usdc_temp_dir
[FILE_MAX
];
586 BLI_path_join(usdc_temp_dir
, FILE_MAX
, BKE_tempdir_session(), "USDZ", SEP_STR
);
588 BLI_assert_msg(BLI_strcasecmp(dir
, usdc_temp_dir
) == 0,
589 "USD Export: Attempting to delete directory that doesn't match the expected "
590 "temporary directory for usdz export.");
591 BLI_delete(usdc_temp_dir
, true, true);
594 static void export_endjob(void *customdata
)
596 ExportJobData
*data
= static_cast<ExportJobData
*>(customdata
);
598 DEG_graph_free(data
->depsgraph
);
600 if (data
->targets_usdz()) {
601 /* NOTE: call to #perform_usdz_conversion has to be done here instead of the main threaded
602 * worker callback (#export_startjob) because USDZ conversion requires changing the current
603 * working directory. This is not safe to do from a non-main thread. Once the USD library fix
604 * this weird requirement, this call can be moved back at the end of #export_startjob, and not
605 * block the main user interface anymore. */
606 bool usd_conversion_success
= perform_usdz_conversion(data
);
607 if (!usd_conversion_success
) {
608 data
->export_ok
= false;
611 export_endjob_usdz_cleanup(data
);
614 if (!data
->export_ok
&& BLI_exists(data
->unarchived_filepath
)) {
615 BLI_delete(data
->unarchived_filepath
, false, false);
618 G
.is_rendering
= false;
620 WM_set_locked_interface(data
->wm
, false);
622 report_job_duration(data
);
626 * To create a USDZ file, we must first create a `.usd/a/c` file and then covert it to `.usdz`.
627 * The temporary files will be created in Blender's temporary session storage.
628 * The `.usdz` file will then be moved to `job->usdz_filepath`.
630 static void create_temp_path_for_usdz_export(const char *filepath
,
631 blender::io::usd::ExportJobData
*job
)
633 char usdc_file
[FILE_MAX
];
634 STRNCPY(usdc_file
, BLI_path_basename(filepath
));
636 if (BLI_path_extension_check(usdc_file
, ".usdz")) {
637 BLI_path_extension_replace(usdc_file
, sizeof(usdc_file
), ".usdc");
640 char usdc_temp_filepath
[FILE_MAX
];
641 BLI_path_join(usdc_temp_filepath
, FILE_MAX
, BKE_tempdir_session(), "USDZ", usdc_file
);
643 STRNCPY(job
->unarchived_filepath
, usdc_temp_filepath
);
644 STRNCPY(job
->usdz_filepath
, filepath
);
647 static void set_job_filepath(blender::io::usd::ExportJobData
*job
, const char *filepath
)
649 if (BLI_path_extension_check_n(filepath
, ".usdz", nullptr)) {
650 create_temp_path_for_usdz_export(filepath
, job
);
654 STRNCPY(job
->unarchived_filepath
, filepath
);
655 job
->usdz_filepath
[0] = '\0';
658 bool USD_export(const bContext
*C
,
659 const char *filepath
,
660 const USDExportParams
*params
,
661 bool as_background_job
,
664 if (!blender::io::usd::export_params_valid(*params
)) {
668 ViewLayer
*view_layer
= CTX_data_view_layer(C
);
669 Scene
*scene
= CTX_data_scene(C
);
671 blender::io::usd::ExportJobData
*job
= static_cast<blender::io::usd::ExportJobData
*>(
672 MEM_mallocN(sizeof(blender::io::usd::ExportJobData
), "ExportJobData"));
674 job
->bmain
= CTX_data_main(C
);
675 job
->wm
= CTX_wm_manager(C
);
677 job
->export_ok
= false;
678 set_job_filepath(job
, filepath
);
680 job
->depsgraph
= DEG_graph_new(job
->bmain
, scene
, view_layer
, params
->evaluation_mode
);
681 job
->params
= *params
;
683 /* Construct the depsgraph for exporting.
685 * Has to be done from main thread currently, as it may affect Main original data (e.g. when
686 * doing deferred update of the view-layers, see #112534 for details). */
687 if (job
->params
.collection
[0]) {
688 Collection
*collection
= reinterpret_cast<Collection
*>(
689 BKE_libblock_find_name(job
->bmain
, ID_GR
, job
->params
.collection
));
691 BKE_reportf(job
->params
.worker_status
->reports
,
693 "USD Export: Unable to find collection '%s'",
694 job
->params
.collection
);
698 DEG_graph_build_from_collection(job
->depsgraph
, collection
);
700 else if (job
->params
.visible_objects_only
) {
701 DEG_graph_build_from_view_layer(job
->depsgraph
);
704 DEG_graph_build_for_all_objects(job
->depsgraph
);
707 bool export_ok
= false;
708 if (as_background_job
) {
709 wmJob
*wm_job
= WM_jobs_get(
710 job
->wm
, CTX_wm_window(C
), scene
, "USD Export", WM_JOB_PROGRESS
, WM_JOB_TYPE_USD_EXPORT
);
713 WM_jobs_customdata_set(wm_job
, job
, MEM_freeN
);
714 WM_jobs_timer(wm_job
, 0.1, NC_SCENE
| ND_FRAME
, NC_SCENE
| ND_FRAME
);
715 WM_jobs_callbacks(wm_job
,
716 blender::io::usd::export_startjob
,
719 blender::io::usd::export_endjob
);
721 WM_jobs_start(CTX_wm_manager(C
), wm_job
);
724 wmJobWorkerStatus worker_status
= {};
725 /* Use the operator's reports in non-background case. */
726 worker_status
.reports
= reports
;
728 blender::io::usd::export_startjob(job
, &worker_status
);
729 blender::io::usd::export_endjob(job
);
730 export_ok
= job
->export_ok
;
738 int USD_get_version()
740 /* USD 19.11 defines:
742 * #define PXR_MAJOR_VERSION 0
743 * #define PXR_MINOR_VERSION 19
744 * #define PXR_PATCH_VERSION 11
745 * #define PXR_VERSION 1911
747 * So the major version is implicit/invisible in the public version number.
752 double get_meters_per_unit(const USDExportParams
¶ms
)
755 switch (params
.convert_scene_units
) {
756 case USD_SCENE_UNITS_CENTIMETERS
:
759 case USD_SCENE_UNITS_MILLIMETERS
:
762 case USD_SCENE_UNITS_KILOMETERS
:
765 case USD_SCENE_UNITS_INCHES
:
768 case USD_SCENE_UNITS_FEET
:
771 case USD_SCENE_UNITS_YARDS
:
774 case USD_SCENE_UNITS_CUSTOM
:
775 result
= double(params
.custom_meters_per_unit
);
785 } // namespace blender::io::usd