Blender V4.3
usd_export_test.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
5#include "testing/testing.h"
7
8#include <pxr/base/plug/registry.h>
9#include <pxr/base/tf/stringUtils.h>
10#include <pxr/base/vt/types.h>
11#include <pxr/base/vt/value.h>
12#include <pxr/usd/sdf/types.h>
13#include <pxr/usd/usd/prim.h>
14#include <pxr/usd/usd/stage.h>
15#include <pxr/usd/usdGeom/mesh.h>
16#include <pxr/usd/usdGeom/subset.h>
17#include <pxr/usd/usdGeom/tokens.h>
18
19#include "DNA_image_types.h"
20#include "DNA_material_types.h"
21#include "DNA_node_types.h"
22
23#include "BKE_context.hh"
24#include "BKE_lib_id.hh"
25#include "BKE_main.hh"
26#include "BKE_mesh.hh"
27#include "BKE_node.hh"
28#include "BLI_fileops.h"
30#include "BLI_path_utils.hh"
31#include "BLO_readfile.hh"
32
33#include "BKE_node_runtime.hh"
34
35#include "DEG_depsgraph.hh"
36
37#include "WM_api.hh"
38
39#include "usd.hh"
40#include "usd_utils.hh"
42
43namespace blender::io::usd {
44
45const StringRefNull simple_scene_filename = "usd/usd_simple_scene.blend";
46const StringRefNull materials_filename = "usd/usd_materials_export.blend";
47const StringRefNull output_filename = "output.usd";
48
49static const bNode *find_node_for_type_in_graph(const bNodeTree *nodetree,
50 const blender::StringRefNull type_idname);
51
53 protected:
54 bContext *context = nullptr;
55
56 public:
58 const eEvaluationMode eval_mode = DAG_EVAL_VIEWPORT)
59 {
60 if (!blendfile_load(filepath.c_str())) {
61 return false;
62 }
63 depsgraph_create(eval_mode);
64
65 context = CTX_create();
66 CTX_data_main_set(context, bfile->main);
68
69 return true;
70 }
71
72 virtual void SetUp() override
73 {
74 BlendfileLoadingBaseTest::SetUp();
75 }
76
77 virtual void TearDown() override
78 {
80 CTX_free(context);
81 context = nullptr;
82
84 BLI_delete(output_filename.c_str(), false, false);
85 }
86 }
87
88 const pxr::UsdPrim get_first_child_mesh(const pxr::UsdPrim prim)
89 {
90 for (auto child : prim.GetChildren()) {
91 if (child.IsA<pxr::UsdGeomMesh>()) {
92 return child;
93 }
94 }
95 return pxr::UsdPrim();
96 }
97
102 const void compare_blender_node_to_usd_prim(const bNode *bsdf_node,
103 const pxr::UsdPrim &bsdf_prim)
104 {
105 ASSERT_NE(bsdf_node, nullptr);
106 ASSERT_TRUE(bool(bsdf_prim));
107
108 for (auto socket : bsdf_node->input_sockets()) {
109 const pxr::TfToken attribute_token = blender::io::usd::token_for_input(socket->name);
110 if (attribute_token.IsEmpty()) {
111 /* This socket is not translated between Blender and USD. */
112 continue;
113 }
114
115 const pxr::UsdAttribute bsdf_attribute = bsdf_prim.GetAttribute(attribute_token);
116 pxr::SdfPathVector paths;
117 bsdf_attribute.GetConnections(&paths);
118 if (!paths.empty() || !bsdf_attribute.IsValid()) {
119 /* Skip if the attribute is connected or has an error. */
120 continue;
121 }
122
123 const float socket_value_f = *socket->default_value_typed<float>();
124 const float3 socket_value_3f = *socket->default_value_typed<float3>();
125 float attribute_value_f;
126 pxr::GfVec3f attribute_value_3f;
127
128 switch (socket->type) {
129 case SOCK_FLOAT:
130 bsdf_attribute.Get(&attribute_value_f, 0.0);
131 EXPECT_FLOAT_EQ(socket_value_f, attribute_value_f);
132 break;
133
134 case SOCK_VECTOR:
135 bsdf_attribute.Get(&attribute_value_3f, 0.0);
136 EXPECT_FLOAT_EQ(socket_value_3f[0], attribute_value_3f[0]);
137 EXPECT_FLOAT_EQ(socket_value_3f[1], attribute_value_3f[1]);
138 EXPECT_FLOAT_EQ(socket_value_3f[2], attribute_value_3f[2]);
139 break;
140
141 case SOCK_RGBA:
142 bsdf_attribute.Get(&attribute_value_3f, 0.0);
143 EXPECT_FLOAT_EQ(socket_value_3f[0], attribute_value_3f[0]);
144 EXPECT_FLOAT_EQ(socket_value_3f[1], attribute_value_3f[1]);
145 EXPECT_FLOAT_EQ(socket_value_3f[2], attribute_value_3f[2]);
146 break;
147
148 default:
149 FAIL() << "Socket " << socket->name << " has unsupported type " << socket->type;
150 break;
151 }
152 }
153 }
154
156 const pxr::UsdPrim &image_prim)
157 {
158 const Image *image = reinterpret_cast<Image *>(image_node->id);
159
160 const pxr::UsdShadeShader image_shader(image_prim);
161 const pxr::UsdShadeInput file_input = image_shader.GetInput(pxr::TfToken("file"));
162 EXPECT_TRUE(bool(file_input));
163
164 pxr::VtValue file_val;
165 EXPECT_TRUE(file_input.Get(&file_val));
166 EXPECT_TRUE(file_val.IsHolding<pxr::SdfAssetPath>());
167
168 pxr::SdfAssetPath image_prim_asset = file_val.Get<pxr::SdfAssetPath>();
169
170 /* The path is expected to be relative, but that means in Blender the
171 * path will start with //.
172 */
173 EXPECT_EQ(
174 BLI_path_cmp_normalized(image->filepath + 2, image_prim_asset.GetAssetPath().c_str()), 0);
175 }
176
177 /*
178 * Determine if a Blender Mesh matches a UsdGeomMesh prim by checking counts
179 * on vertices, faces, face indices, and normals.
180 */
181 const void compare_blender_mesh_to_usd_prim(const Mesh *mesh, const pxr::UsdGeomMesh &mesh_prim)
182 {
183 pxr::VtIntArray face_indices;
184 pxr::VtIntArray face_counts;
185 pxr::VtVec3fArray positions;
186 pxr::VtVec3fArray normals;
187
188 /* Our export doesn't use 'primvars:normals' so we're not
189 * looking for that to be written here. */
190 mesh_prim.GetFaceVertexIndicesAttr().Get(&face_indices, 0.0);
191 mesh_prim.GetFaceVertexCountsAttr().Get(&face_counts, 0.0);
192 mesh_prim.GetPointsAttr().Get(&positions, 0.0);
193 mesh_prim.GetNormalsAttr().Get(&normals, 0.0);
194
195 EXPECT_EQ(mesh->verts_num, positions.size());
196 EXPECT_EQ(mesh->faces_num, face_counts.size());
197 EXPECT_EQ(mesh->corners_num, face_indices.size());
198 EXPECT_EQ(mesh->corners_num, normals.size());
199 }
200};
201
202TEST_F(UsdExportTest, usd_export_rain_mesh)
203{
204 if (!load_file_and_depsgraph(simple_scene_filename)) {
205 FAIL() << "Unable to load file: " << simple_scene_filename;
206 return;
207 }
208
209 /* File sanity check. */
210 EXPECT_EQ(BLI_listbase_count(&bfile->main->objects), 3);
211
213 params.export_materials = false;
214 params.export_normals = true;
215 params.export_uvmaps = false;
216 params.visible_objects_only = true;
217
218 bool result = USD_export(context, output_filename.c_str(), &params, false, nullptr);
219 ASSERT_TRUE(result) << "Writing to " << output_filename << " failed!";
220
221 pxr::UsdStageRefPtr stage = pxr::UsdStage::Open(output_filename);
222 ASSERT_TRUE(bool(stage)) << "Unable to load Stage from " << output_filename;
223
224 /*
225 * Run the mesh comparison for all Meshes in the original scene.
226 */
227 LISTBASE_FOREACH (Object *, object, &bfile->main->objects) {
228 const Mesh *mesh = static_cast<Mesh *>(object->data);
229 const StringRefNull object_name(object->id.name + 2);
230
231 const pxr::SdfPath sdf_path("/" + pxr::TfMakeValidIdentifier(object_name.c_str()));
232 pxr::UsdPrim prim = stage->GetPrimAtPath(sdf_path);
233 EXPECT_TRUE(bool(prim));
234
235 const pxr::UsdGeomMesh mesh_prim(get_first_child_mesh(prim));
236 EXPECT_TRUE(bool(mesh_prim));
237
238 compare_blender_mesh_to_usd_prim(mesh, mesh_prim);
239 }
240}
241
242static const bNode *find_node_for_type_in_graph(const bNodeTree *nodetree,
243 const blender::StringRefNull type_idname)
244{
245 auto found_nodes = nodetree->nodes_by_type(type_idname);
246 if (found_nodes.size() == 1) {
247 return found_nodes[0];
248 }
249
250 return nullptr;
251}
252
253/*
254 * Export Material test-- export a scene with a material, then read it back
255 * in and check that the BSDF and Image Texture nodes translated correctly
256 * by comparing values between the exported USD stage and the objects in
257 * memory.
258 */
259TEST_F(UsdExportTest, usd_export_material)
260{
261 if (!load_file_and_depsgraph(materials_filename)) {
262 FAIL() << "Unable to load file: " << materials_filename;
263 return;
264 }
265
266 /* File sanity checks. */
267 EXPECT_EQ(BLI_listbase_count(&bfile->main->objects), 6);
268 /* There is 1 additional material because of the "Dots Stroke". */
269 EXPECT_EQ(BLI_listbase_count(&bfile->main->materials), 7);
270
271 Material *material = reinterpret_cast<Material *>(
272 BKE_libblock_find_name(bfile->main, ID_MA, "Material"));
273
274 EXPECT_TRUE(bool(material));
275
277 params.export_materials = true;
278 params.export_normals = true;
279 params.export_textures = false;
280 params.export_uvmaps = true;
281 params.generate_preview_surface = true;
282 params.generate_materialx_network = false;
283 params.convert_world_material = false;
284 params.relative_paths = false;
285
286 const bool result = USD_export(context, output_filename.c_str(), &params, false, nullptr);
287 ASSERT_TRUE(result) << "Unable to export stage to " << output_filename;
288
289 pxr::UsdStageRefPtr stage = pxr::UsdStage::Open(output_filename);
290 ASSERT_NE(stage, nullptr) << "Unable to open exported stage: " << output_filename;
291
292 material->nodetree->ensure_topology_cache();
293 const bNode *bsdf_node = find_node_for_type_in_graph(material->nodetree,
294 "ShaderNodeBsdfPrincipled");
295
296 const std::string prim_name = pxr::TfMakeValidIdentifier(bsdf_node->name);
297 const pxr::UsdPrim bsdf_prim = stage->GetPrimAtPath(
298 pxr::SdfPath("/_materials/Material/" + prim_name));
299
300 compare_blender_node_to_usd_prim(bsdf_node, bsdf_prim);
301
302 const bNode *image_node = find_node_for_type_in_graph(material->nodetree, "ShaderNodeTexImage");
303 ASSERT_NE(image_node, nullptr);
304 ASSERT_NE(image_node->storage, nullptr);
305
306 const std::string image_prim_name = pxr::TfMakeValidIdentifier(image_node->name);
307
308 const pxr::UsdPrim image_prim = stage->GetPrimAtPath(
309 pxr::SdfPath("/_materials/Material/" + image_prim_name));
310
311 ASSERT_TRUE(bool(image_prim)) << "Unable to find Material prim from exported stage "
313
314 compare_blender_image_to_usd_image_shader(image_node, image_prim);
315}
316
318{
319 ASSERT_EQ(make_safe_name("", false), std::string("_"));
320 ASSERT_EQ(make_safe_name("1", false), std::string("_"));
321 ASSERT_EQ(make_safe_name("1Test", false), std::string("_Test"));
322
323 ASSERT_EQ(make_safe_name("Test", false), std::string("Test"));
324 ASSERT_EQ(make_safe_name("Test|$bézier @ world", false), std::string("Test__b__zier___world"));
325 ASSERT_EQ(make_safe_name("Test|ハローワールド", false),
326 std::string("Test______________________"));
327 ASSERT_EQ(make_safe_name("Test|Γεια σου κόσμε", false),
328 std::string("Test___________________________"));
329 ASSERT_EQ(make_safe_name("Test|∧hello ○ wórld", false), std::string("Test____hello_____w__rld"));
330
331#if PXR_VERSION >= 2403
332 ASSERT_EQ(make_safe_name("", true), std::string("_"));
333 ASSERT_EQ(make_safe_name("1", true), std::string("_"));
334 ASSERT_EQ(make_safe_name("1Test", true), std::string("_Test"));
335
336 ASSERT_EQ(make_safe_name("Test", true), std::string("Test"));
337 ASSERT_EQ(make_safe_name("Test|$bézier @ world", true), std::string("Test__bézier___world"));
338 ASSERT_EQ(make_safe_name("Test|ハローワールド", true), std::string("Test_ハローワールド"));
339 ASSERT_EQ(make_safe_name("Test|Γεια σου κόσμε", true), std::string("Test_Γεια_σου_κόσμε"));
340 ASSERT_EQ(make_safe_name("Test|∧hello ○ wórld", true), std::string("Test__hello___wórld"));
341#endif
342}
343
344} // namespace blender::io::usd
void CTX_data_main_set(bContext *C, Main *bmain)
void CTX_free(bContext *C)
void CTX_data_scene_set(bContext *C, Scene *scene)
bContext * CTX_create()
ID * BKE_libblock_find_name(Main *bmain, short type, const char *name, const std::optional< Library * > lib=std::nullopt) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL()
Definition lib_id.cc:1657
EXPECT_EQ(BLI_expr_pylike_eval(expr, nullptr, 0, &result), EXPR_PYLIKE_INVALID)
File and directory operations.
int BLI_exists(const char *path) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL()
Definition storage.cc:350
int BLI_delete(const char *path, bool dir, bool recursive) ATTR_NONNULL()
#define LISTBASE_FOREACH(type, var, list)
int BLI_listbase_count(const struct ListBase *listbase) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL(1)
int BLI_path_cmp_normalized(const char *p1, const char *p2) ATTR_NONNULL(1
external readfile function prototypes.
eEvaluationMode
@ DAG_EVAL_VIEWPORT
@ ID_MA
@ SOCK_VECTOR
@ SOCK_FLOAT
@ SOCK_RGBA
virtual void depsgraph_create(eEvaluationMode depsgraph_evaluation_mode)
bool blendfile_load(const char *filepath)
constexpr const char * c_str() const
virtual void TearDown() override
const void compare_blender_mesh_to_usd_prim(const Mesh *mesh, const pxr::UsdGeomMesh &mesh_prim)
const void compare_blender_node_to_usd_prim(const bNode *bsdf_node, const pxr::UsdPrim &bsdf_prim)
const void compare_blender_image_to_usd_image_shader(const bNode *image_node, const pxr::UsdPrim &image_prim)
virtual void SetUp() override
const pxr::UsdPrim get_first_child_mesh(const pxr::UsdPrim prim)
bool load_file_and_depsgraph(const StringRefNull &filepath, const eEvaluationMode eval_mode=DAG_EVAL_VIEWPORT)
EvaluationStage stage
Definition deg_eval.cc:83
static float normals[][3]
uiWidgetBaseParameters params[MAX_WIDGET_BASE_BATCH]
pxr::TfToken token_for_input(const char *input_name)
static const bNode * find_node_for_type_in_graph(const bNodeTree *nodetree, const blender::StringRefNull type_idname)
std::string make_safe_name(const std::string &name, bool allow_unicode)
Definition usd_utils.cc:16
TEST(utilities, make_safe_name)
const StringRefNull simple_scene_filename
const StringRefNull materials_filename
bool USD_export(const bContext *C, const char *filepath, const USDExportParams *params, bool as_background_job, ReportList *reports)
const StringRefNull output_filename
TEST_F(UsdCurvesTest, usd_export_curves)
struct ID * id
char name[64]
void * storage