USD IO CI Tests
Various new CI tests for USD Import / Export functionalty: Import: - Added mesh import tests for topology types and multiple UV sets. (Python) Export: - Added a verification tests for mesh topology. (C++) - Added a test to make sure UsdPreviewSurface export conversion of materials is correct. (C++) Reviewed by: Sybren and Hans. Differential Revision: https://developer.blender.org/D16274
This commit is contained in:
parent
feae1c7d05
commit
fa57c691f6
|
@ -103,8 +103,8 @@ void BlendfileLoadingBaseTest::TearDownTestCase()
|
|||
void BlendfileLoadingBaseTest::TearDown()
|
||||
{
|
||||
BKE_mball_cubeTable_free();
|
||||
depsgraph_free();
|
||||
blendfile_free();
|
||||
depsgraph_free();
|
||||
|
||||
testing::Test::TearDown();
|
||||
}
|
||||
|
|
|
@ -163,13 +163,18 @@ target_link_libraries(bf_usd INTERFACE ${TBB_LIBRARIES})
|
|||
if(WITH_GTESTS)
|
||||
set(TEST_SRC
|
||||
tests/usd_stage_creation_test.cc
|
||||
tests/usd_export_test.cc
|
||||
tests/usd_tests_common.cc
|
||||
tests/usd_tests_common.h
|
||||
|
||||
intern/usd_writer_material.h
|
||||
)
|
||||
if(USD_IMAGING_HEADERS)
|
||||
list(APPEND TEST_SRC tests/usd_imaging_test.cc)
|
||||
endif()
|
||||
|
||||
include_directories(intern)
|
||||
|
||||
set(TEST_INC
|
||||
)
|
||||
set(TEST_LIB
|
||||
|
|
|
@ -66,7 +66,9 @@ static void export_startjob(void *customdata,
|
|||
data->start_time = timeit::Clock::now();
|
||||
|
||||
G.is_rendering = true;
|
||||
WM_set_locked_interface(data->wm, true);
|
||||
if (data->wm) {
|
||||
WM_set_locked_interface(data->wm, true);
|
||||
}
|
||||
G.is_break = false;
|
||||
|
||||
/* Construct the depsgraph for exporting. */
|
||||
|
@ -160,7 +162,9 @@ static void export_endjob(void *customdata)
|
|||
}
|
||||
|
||||
G.is_rendering = false;
|
||||
WM_set_locked_interface(data->wm, false);
|
||||
if (data->wm) {
|
||||
WM_set_locked_interface(data->wm, false);
|
||||
}
|
||||
report_job_duration(data);
|
||||
}
|
||||
|
||||
|
|
|
@ -207,6 +207,7 @@ static void import_startjob(void *customdata, bool *stop, bool *do_update, float
|
|||
if (!stage) {
|
||||
WM_reportf(RPT_ERROR, "USD Import: unable to open stage to read %s", data->filepath);
|
||||
data->import_ok = false;
|
||||
data->error_code = USD_ARCHIVE_FAIL;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -748,4 +748,16 @@ static void export_texture(bNode *node,
|
|||
}
|
||||
}
|
||||
|
||||
const pxr::TfToken token_for_input(const char *input_name)
|
||||
{
|
||||
const InputSpecMap &input_map = preview_surface_input_map();
|
||||
const InputSpecMap::const_iterator it = input_map.find(input_name);
|
||||
|
||||
if (it == input_map.end()) {
|
||||
return pxr::TfToken();
|
||||
}
|
||||
|
||||
return it->second.input_name;
|
||||
}
|
||||
|
||||
} // namespace blender::io::usd
|
||||
|
|
|
@ -14,6 +14,10 @@ namespace blender::io::usd {
|
|||
|
||||
struct USDExporterContext;
|
||||
|
||||
/* Returns a USDPreviewSurface token name for a given Blender shader Socket name,
|
||||
* or an empty TfToken if the input name is not found in the map. */
|
||||
const pxr::TfToken token_for_input(const char *input_name);
|
||||
|
||||
/**
|
||||
* Entry point to create an approximate USD Preview Surface network from a Cycles node graph.
|
||||
* Due to the limited nodes in the USD Preview Surface specification, only the following nodes
|
||||
|
|
|
@ -0,0 +1,314 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "testing/testing.h"
|
||||
#include "tests/blendfile_loading_base_test.h"
|
||||
|
||||
#include <pxr/base/plug/registry.h>
|
||||
#include <pxr/base/tf/stringUtils.h>
|
||||
#include <pxr/base/vt/types.h>
|
||||
#include <pxr/base/vt/value.h>
|
||||
#include <pxr/usd/sdf/types.h>
|
||||
#include <pxr/usd/usd/prim.h>
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/usdGeom/mesh.h>
|
||||
#include <pxr/usd/usdGeom/subset.h>
|
||||
#include <pxr/usd/usdGeom/tokens.h>
|
||||
|
||||
#include "DNA_image_types.h"
|
||||
#include "DNA_material_types.h"
|
||||
#include "DNA_node_types.h"
|
||||
|
||||
#include "BKE_context.h"
|
||||
#include "BKE_lib_id.h"
|
||||
#include "BKE_main.h"
|
||||
#include "BKE_mesh.h"
|
||||
#include "BKE_node.h"
|
||||
#include "BLI_fileops.h"
|
||||
#include "BLI_math.h"
|
||||
#include "BLI_path_util.h"
|
||||
#include "BLI_math_vector_types.hh"
|
||||
#include "BLO_readfile.h"
|
||||
|
||||
#include "BKE_node_runtime.hh"
|
||||
|
||||
#include "DEG_depsgraph.h"
|
||||
|
||||
#include "WM_api.h"
|
||||
|
||||
#include "usd.h"
|
||||
#include "usd_tests_common.h"
|
||||
#include "usd_writer_material.h"
|
||||
|
||||
namespace blender::io::usd {
|
||||
|
||||
const StringRefNull simple_scene_filename = "usd/usd_simple_scene.blend";
|
||||
const StringRefNull materials_filename = "usd/usd_materials_export.blend";
|
||||
const StringRefNull output_filename = "output.usd";
|
||||
|
||||
|
||||
static const bNode *find_node_for_type_in_graph(const bNodeTree *nodetree,
|
||||
const blender::StringRefNull type_idname);
|
||||
|
||||
|
||||
class UsdExportTest : public BlendfileLoadingBaseTest {
|
||||
protected:
|
||||
struct bContext *context = nullptr;
|
||||
|
||||
public:
|
||||
bool load_file_and_depsgraph(const StringRefNull &filepath,
|
||||
const eEvaluationMode eval_mode = DAG_EVAL_VIEWPORT)
|
||||
{
|
||||
if (!blendfile_load(filepath.c_str())) {
|
||||
return false;
|
||||
}
|
||||
depsgraph_create(eval_mode);
|
||||
|
||||
context = CTX_create();
|
||||
CTX_data_main_set(context, bfile->main);
|
||||
CTX_data_scene_set(context, bfile->curscene);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void SetUp() override
|
||||
{
|
||||
BlendfileLoadingBaseTest::SetUp();
|
||||
std::string usd_plugin_path = register_usd_plugins_for_tests();
|
||||
if (usd_plugin_path.empty()) {
|
||||
FAIL() << "Unable to find the USD Plugins path.";
|
||||
}
|
||||
}
|
||||
|
||||
virtual void TearDown() override
|
||||
{
|
||||
BlendfileLoadingBaseTest::TearDown();
|
||||
CTX_free(context);
|
||||
context = nullptr;
|
||||
|
||||
if (BLI_exists(output_filename.c_str())) {
|
||||
BLI_delete(output_filename.c_str(), false, false);
|
||||
}
|
||||
}
|
||||
|
||||
const pxr::UsdPrim get_first_child_mesh(const pxr::UsdPrim prim)
|
||||
{
|
||||
for (auto child : prim.GetChildren()) {
|
||||
if (child.IsA<pxr::UsdGeomMesh>()) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return pxr::UsdPrim();
|
||||
}
|
||||
|
||||
/*
|
||||
* Loop the sockets on the Blender bNode, and fail if any of their values do
|
||||
* not match the equivalent Attribtue values on the UsdPrim.
|
||||
*/
|
||||
const void compare_blender_node_to_usd_prim(const bNode *bsdf_node, const pxr::UsdPrim& bsdf_prim) {
|
||||
ASSERT_NE(bsdf_node, nullptr);
|
||||
ASSERT_TRUE(bool(bsdf_prim));
|
||||
|
||||
for (auto socket : bsdf_node->input_sockets()) {
|
||||
const pxr::TfToken attribute_token = blender::io::usd::token_for_input(socket->name);
|
||||
if (attribute_token.IsEmpty()) {
|
||||
/* This socket is not translated between Blender and USD. */
|
||||
continue;
|
||||
}
|
||||
|
||||
const pxr::UsdAttribute bsdf_attribute = bsdf_prim.GetAttribute(attribute_token);
|
||||
pxr::SdfPathVector paths;
|
||||
bsdf_attribute.GetConnections(&paths);
|
||||
if (!paths.empty() || !bsdf_attribute.IsValid()) {
|
||||
/* Skip if the attribute is connected or has an error. */
|
||||
continue;
|
||||
}
|
||||
|
||||
const float socket_value_f = *socket->default_value_typed<float>();
|
||||
const float3 socket_value_3f = *socket->default_value_typed<float3>();
|
||||
float attribute_value_f;
|
||||
pxr::GfVec3f attribute_value_3f;
|
||||
|
||||
switch (socket->type) {
|
||||
case SOCK_FLOAT:
|
||||
bsdf_attribute.Get(&attribute_value_f, 0.0);
|
||||
EXPECT_FLOAT_EQ(socket_value_f, attribute_value_f);
|
||||
break;
|
||||
|
||||
case SOCK_VECTOR:
|
||||
bsdf_attribute.Get(&attribute_value_3f, 0.0);
|
||||
EXPECT_FLOAT_EQ(socket_value_3f[0], attribute_value_3f[0]);
|
||||
EXPECT_FLOAT_EQ(socket_value_3f[1], attribute_value_3f[1]);
|
||||
EXPECT_FLOAT_EQ(socket_value_3f[2], attribute_value_3f[2]);
|
||||
break;
|
||||
|
||||
case SOCK_RGBA:
|
||||
bsdf_attribute.Get(&attribute_value_3f, 0.0);
|
||||
EXPECT_FLOAT_EQ(socket_value_3f[0], attribute_value_3f[0]);
|
||||
EXPECT_FLOAT_EQ(socket_value_3f[1], attribute_value_3f[1]);
|
||||
EXPECT_FLOAT_EQ(socket_value_3f[2], attribute_value_3f[2]);
|
||||
break;
|
||||
|
||||
default:
|
||||
FAIL() << "Socket " << socket->name << " has unsupported type " << socket->type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const void compare_blender_image_to_usd_image_shader(const bNode *image_node, const pxr::UsdPrim& image_prim) {
|
||||
const Image *image = reinterpret_cast<Image *>(image_node->id);
|
||||
|
||||
const pxr::UsdShadeShader image_shader(image_prim);
|
||||
const pxr::UsdShadeInput file_input = image_shader.GetInput(pxr::TfToken("file"));
|
||||
EXPECT_TRUE(bool(file_input));
|
||||
|
||||
pxr::VtValue file_val;
|
||||
EXPECT_TRUE(file_input.Get(&file_val));
|
||||
EXPECT_TRUE(file_val.IsHolding<pxr::SdfAssetPath>());
|
||||
|
||||
pxr::SdfAssetPath image_prim_asset = file_val.Get<pxr::SdfAssetPath>();
|
||||
|
||||
/* The path is expected to be relative, but that means in Blender the
|
||||
* path will start with //.
|
||||
*/
|
||||
EXPECT_EQ(BLI_path_cmp_normalized(image->filepath+2, image_prim_asset.GetAssetPath().c_str()), 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* Determine if a Blender Mesh matches a UsdGeomMesh prim by checking counts
|
||||
* on vertices, faces, face indices, and normals.
|
||||
*/
|
||||
const void compare_blender_mesh_to_usd_prim(const Mesh *mesh, const pxr::UsdGeomMesh& mesh_prim) {
|
||||
pxr::VtIntArray face_indices;
|
||||
pxr::VtIntArray face_counts;
|
||||
pxr::VtVec3fArray positions;
|
||||
pxr::VtVec3fArray normals;
|
||||
|
||||
/* Our export doesn't use 'primvars:normals' so we're not
|
||||
* looking for that to be written here. */
|
||||
mesh_prim.GetFaceVertexIndicesAttr().Get(&face_indices, 0.0);
|
||||
mesh_prim.GetFaceVertexCountsAttr().Get(&face_counts, 0.0);
|
||||
mesh_prim.GetPointsAttr().Get(&positions, 0.0);
|
||||
mesh_prim.GetNormalsAttr().Get(&normals, 0.0);
|
||||
|
||||
EXPECT_EQ(mesh->totvert, positions.size());
|
||||
EXPECT_EQ(mesh->totpoly, face_counts.size());
|
||||
EXPECT_EQ(mesh->totloop, face_indices.size());
|
||||
EXPECT_EQ(mesh->totloop, normals.size());
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
TEST_F(UsdExportTest, usd_export_rain_mesh)
|
||||
{
|
||||
if (!load_file_and_depsgraph(simple_scene_filename)) {
|
||||
FAIL() << "Unable to load file: " << simple_scene_filename;
|
||||
return;
|
||||
}
|
||||
|
||||
/* File sanity check. */
|
||||
EXPECT_EQ(BLI_listbase_count(&bfile->main->objects), 3);
|
||||
|
||||
USDExportParams params{};
|
||||
params.export_normals = true;
|
||||
params.visible_objects_only = true;
|
||||
params.evaluation_mode = eEvaluationMode::DAG_EVAL_VIEWPORT;
|
||||
|
||||
bool result = USD_export(context, output_filename.c_str(), ¶ms, false);
|
||||
ASSERT_TRUE(result) << "Writing to " << output_filename << " failed!";
|
||||
|
||||
pxr::UsdStageRefPtr stage = pxr::UsdStage::Open(output_filename);
|
||||
ASSERT_TRUE(bool(stage)) << "Unable to load Stage from " << output_filename;
|
||||
|
||||
/*
|
||||
* Run the mesh comparison for all Meshes in the original scene.
|
||||
*/
|
||||
LISTBASE_FOREACH (Object *, object, &bfile->main->objects) {
|
||||
const Mesh *mesh = static_cast<Mesh *>(object->data);
|
||||
const StringRefNull object_name(object->id.name + 2);
|
||||
|
||||
const pxr::SdfPath sdf_path("/" + pxr::TfMakeValidIdentifier(object_name.c_str()));
|
||||
pxr::UsdPrim prim = stage->GetPrimAtPath(sdf_path);
|
||||
EXPECT_TRUE(bool(prim));
|
||||
|
||||
const pxr::UsdGeomMesh mesh_prim(get_first_child_mesh(prim));
|
||||
EXPECT_TRUE(bool(mesh_prim));
|
||||
|
||||
compare_blender_mesh_to_usd_prim(mesh, mesh_prim);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static const bNode *find_node_for_type_in_graph(const bNodeTree *nodetree, const blender::StringRefNull type_idname)
|
||||
{
|
||||
auto found_nodes = nodetree->nodes_by_type(type_idname);
|
||||
if (found_nodes.size() == 1) {
|
||||
return found_nodes[0];
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Export Material test-- export a scene with a material, then read it back
|
||||
* in and check that the BSDF and Image Texture nodes translated correctly
|
||||
* by comparing values between the exported USD stage and the objects in
|
||||
* memory.
|
||||
*/
|
||||
TEST_F(UsdExportTest, usd_export_material)
|
||||
{
|
||||
if (!load_file_and_depsgraph(materials_filename)) {
|
||||
FAIL() << "Unable to load file: " << materials_filename;
|
||||
return;
|
||||
}
|
||||
|
||||
/* File sanity checks. */
|
||||
EXPECT_EQ(BLI_listbase_count(&bfile->main->objects), 1);
|
||||
/* There are two materials because of the Dots Stroke. */
|
||||
EXPECT_EQ(BLI_listbase_count(&bfile->main->materials), 2);
|
||||
|
||||
Material *material = reinterpret_cast<Material *>(BKE_libblock_find_name(bfile->main, ID_MA, "Material"));
|
||||
|
||||
EXPECT_TRUE(bool(material));
|
||||
|
||||
USDExportParams params{};
|
||||
params.export_normals = true;
|
||||
params.export_materials = true;
|
||||
params.generate_preview_surface = true;
|
||||
params.export_uvmaps = true;
|
||||
params.evaluation_mode = eEvaluationMode::DAG_EVAL_VIEWPORT;
|
||||
|
||||
const bool result = USD_export(context, output_filename.c_str(), ¶ms, false);
|
||||
ASSERT_TRUE(result) << "Unable to export stage to " << output_filename;
|
||||
|
||||
pxr::UsdStageRefPtr stage = pxr::UsdStage::Open(output_filename);
|
||||
ASSERT_NE(stage, nullptr) << "Unable to open exported stage: " << output_filename;
|
||||
|
||||
material->nodetree->ensure_topology_cache();
|
||||
const bNode *bsdf_node = find_node_for_type_in_graph(material->nodetree,
|
||||
"ShaderNodeBsdfPrincipled");
|
||||
|
||||
const std::string prim_name = pxr::TfMakeValidIdentifier(bsdf_node->name);
|
||||
const pxr::UsdPrim bsdf_prim = stage->GetPrimAtPath(
|
||||
pxr::SdfPath("/_materials/Material/preview/" + prim_name));
|
||||
|
||||
compare_blender_node_to_usd_prim(bsdf_node, bsdf_prim);
|
||||
|
||||
const bNode *image_node = find_node_for_type_in_graph(material->nodetree, "ShaderNodeTexImage");
|
||||
ASSERT_NE(image_node, nullptr);
|
||||
ASSERT_NE(image_node->storage, nullptr);
|
||||
|
||||
|
||||
const std::string image_prim_name = pxr::TfMakeValidIdentifier(image_node->name);
|
||||
|
||||
const pxr::UsdPrim image_prim = stage->GetPrimAtPath(
|
||||
pxr::SdfPath("/_materials/Material/preview/" + image_prim_name));
|
||||
|
||||
ASSERT_TRUE(bool(image_prim)) << "Unable to find Material prim from exported stage " << output_filename;
|
||||
|
||||
compare_blender_image_to_usd_image_shader(image_node, image_prim);
|
||||
}
|
||||
|
||||
} // namespace blender::io::usd
|
|
@ -24,24 +24,132 @@ class AbstractUSDTest(unittest.TestCase):
|
|||
|
||||
class USDImportTest(AbstractUSDTest):
|
||||
|
||||
def test_import_operator(self):
|
||||
"""Test running the import operator on valid and invalid files."""
|
||||
|
||||
infile = str(self.testdir / "usd_mesh_polygon_types.usda")
|
||||
res = bpy.ops.wm.usd_import(filepath=infile)
|
||||
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {infile}")
|
||||
|
||||
infile = str(self.testdir / "this_file_doesn't_exist.usda")
|
||||
res = bpy.ops.wm.usd_import(filepath=infile)
|
||||
self.assertEqual({'CANCELLED'}, res, "Was somehow able to import a non-existent USD file!")
|
||||
|
||||
def test_import_prim_hierarchy(self):
|
||||
"""Test importing a simple object hierarchy from a USDA file."""
|
||||
|
||||
infile = str(self.testdir / "prim-hierarchy.usda")
|
||||
|
||||
res = bpy.ops.wm.usd_import(filepath=infile)
|
||||
self.assertEqual({'FINISHED'}, res)
|
||||
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {infile}")
|
||||
|
||||
objects = bpy.context.scene.collection.objects
|
||||
self.assertEqual(5, len(objects))
|
||||
self.assertEqual(5, len(objects), f"Test scene {infile} should have five objects; found {len(objects)}")
|
||||
|
||||
# Test the hierarchy.
|
||||
self.assertIsNone(objects['World'].parent)
|
||||
self.assertEqual(objects['World'], objects['Plane'].parent)
|
||||
self.assertEqual(objects['World'], objects['Plane_001'].parent)
|
||||
self.assertEqual(objects['World'], objects['Empty'].parent)
|
||||
self.assertEqual(objects['Empty'], objects['Plane_002'].parent)
|
||||
self.assertIsNone(objects['World'].parent, "/World should not be parented.")
|
||||
self.assertEqual(objects['World'], objects['Plane'].parent, "Plane should be child of /World")
|
||||
self.assertEqual(objects['World'], objects['Plane_001'].parent, "Plane_001 should be a child of /World")
|
||||
self.assertEqual(objects['World'], objects['Empty'].parent, "Empty should be a child of /World")
|
||||
self.assertEqual(objects['Empty'], objects['Plane_002'].parent, "Plane_002 should be a child of /World")
|
||||
|
||||
def test_import_mesh_topology(self):
|
||||
"""Test importing meshes with different polygon types."""
|
||||
|
||||
infile = str(self.testdir / "usd_mesh_polygon_types.usda")
|
||||
res = bpy.ops.wm.usd_import(filepath=infile)
|
||||
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {infile}")
|
||||
|
||||
objects = bpy.context.scene.collection.objects
|
||||
self.assertEqual(5, len(objects), f"Test scene {infile} should have five objects; found {len(objects)}")
|
||||
|
||||
# Test topology counts.
|
||||
self.assertIn("m_degenerate", objects, "Scene does not contain object m_degenerate")
|
||||
mesh = objects["m_degenerate"].data
|
||||
self.assertEqual(len(mesh.polygons), 2)
|
||||
self.assertEqual(len(mesh.edges), 7)
|
||||
self.assertEqual(len(mesh.vertices), 6)
|
||||
|
||||
self.assertIn("m_triangles", objects, "Scene does not contain object m_triangles")
|
||||
mesh = objects["m_triangles"].data
|
||||
self.assertEqual(len(mesh.polygons), 2)
|
||||
self.assertEqual(len(mesh.edges), 5)
|
||||
self.assertEqual(len(mesh.vertices), 4)
|
||||
self.assertEqual(len(mesh.polygons[0].vertices), 3)
|
||||
|
||||
self.assertIn("m_quad", objects, "Scene does not contain object m_quad")
|
||||
mesh = objects["m_quad"].data
|
||||
self.assertEqual(len(mesh.polygons), 1)
|
||||
self.assertEqual(len(mesh.edges), 4)
|
||||
self.assertEqual(len(mesh.vertices), 4)
|
||||
self.assertEqual(len(mesh.polygons[0].vertices), 4)
|
||||
|
||||
self.assertIn("m_ngon_concave", objects, "Scene does not contain object m_ngon_concave")
|
||||
mesh = objects["m_ngon_concave"].data
|
||||
self.assertEqual(len(mesh.polygons), 1)
|
||||
self.assertEqual(len(mesh.edges), 5)
|
||||
self.assertEqual(len(mesh.vertices), 5)
|
||||
self.assertEqual(len(mesh.polygons[0].vertices), 5)
|
||||
|
||||
self.assertIn("m_ngon_convex", objects, "Scene does not contain object m_ngon_convex")
|
||||
mesh = objects["m_ngon_convex"].data
|
||||
self.assertEqual(len(mesh.polygons), 1)
|
||||
self.assertEqual(len(mesh.edges), 5)
|
||||
self.assertEqual(len(mesh.vertices), 5)
|
||||
self.assertEqual(len(mesh.polygons[0].vertices), 5)
|
||||
|
||||
def test_import_mesh_uv_maps(self):
|
||||
"""Test importing meshes with udim UVs and multiple UV sets."""
|
||||
|
||||
infile = str(self.testdir / "usd_mesh_udim.usda")
|
||||
res = bpy.ops.wm.usd_import(filepath=infile)
|
||||
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {infile}")
|
||||
|
||||
objects = bpy.context.scene.collection.objects
|
||||
if "preview" in bpy.data.objects:
|
||||
bpy.data.objects.remove(bpy.data.objects["preview"])
|
||||
self.assertEqual(1, len(objects), f"File {infile} should contain one object, found {len(objects)}")
|
||||
|
||||
mesh = bpy.data.objects["uvmap_plane"].data
|
||||
self.assertEqual(len(mesh.uv_layers), 2, f"Object uvmap_plane should have two uv layers, found {len(mesh.uv_layers)}")
|
||||
|
||||
expected_layer_names = {"udim_map", "uvmap"}
|
||||
imported_layer_names = set(mesh.uv_layers.keys())
|
||||
self.assertEqual(expected_layer_names, imported_layer_names, f"Expected layer names ({expected_layer_names}) not found on uvmap_plane.")
|
||||
|
||||
def get_coords(data):
|
||||
coords = [x.uv for x in uvmap]
|
||||
return coords
|
||||
|
||||
def uv_min_max(data):
|
||||
coords = get_coords(data)
|
||||
uv_min_x = min([uv[0] for uv in coords])
|
||||
uv_max_x = max([uv[0] for uv in coords])
|
||||
uv_min_y = min([uv[1] for uv in coords])
|
||||
uv_max_y = max([uv[1] for uv in coords])
|
||||
return uv_min_x, uv_max_x, uv_min_y, uv_max_y
|
||||
|
||||
## Quick tests for point range.
|
||||
uvmap = mesh.uv_layers["uvmap"].data
|
||||
self.assertEqual(len(uvmap), 128)
|
||||
min_x, max_x, min_y, max_y = uv_min_max(uvmap)
|
||||
self.assertGreaterEqual(min_x, 0.0)
|
||||
self.assertGreaterEqual(min_y, 0.0)
|
||||
self.assertLessEqual(max_x, 1.0)
|
||||
self.assertLessEqual(max_y, 1.0)
|
||||
|
||||
uvmap = mesh.uv_layers["udim_map"].data
|
||||
self.assertEqual(len(uvmap), 128)
|
||||
min_x, max_x, min_y, max_y = uv_min_max(uvmap)
|
||||
self.assertGreaterEqual(min_x, 0.0)
|
||||
self.assertGreaterEqual(min_y, 0.0)
|
||||
self.assertLessEqual(max_x, 2.0)
|
||||
self.assertLessEqual(max_y, 1.0)
|
||||
|
||||
## Make sure at least some points are in a udim tile.
|
||||
coords = get_coords(uvmap)
|
||||
coords = list(filter(lambda x: x[0] > 1.0, coords))
|
||||
self.assertGreater(len(coords), 16)
|
||||
|
||||
def main():
|
||||
global args
|
||||
|
|
Loading…
Reference in New Issue