forked from blender/blender
WIP: uv-simple-select #1
@ -5,12 +5,15 @@
|
|||||||
#include "usd_writer_material.h"
|
#include "usd_writer_material.h"
|
||||||
|
|
||||||
#include <pxr/base/tf/stringUtils.h>
|
#include <pxr/base/tf/stringUtils.h>
|
||||||
|
#include <pxr/usd/usdGeom/bboxCache.h>
|
||||||
|
|
||||||
#include "BKE_customdata.h"
|
#include "BKE_customdata.h"
|
||||||
#include "BLI_assert.h"
|
#include "BLI_assert.h"
|
||||||
|
|
||||||
#include "DNA_mesh_types.h"
|
#include "DNA_mesh_types.h"
|
||||||
|
|
||||||
|
#include "WM_api.h"
|
||||||
|
|
||||||
/* TfToken objects are not cheap to construct, so we do it once. */
|
/* TfToken objects are not cheap to construct, so we do it once. */
|
||||||
namespace usdtokens {
|
namespace usdtokens {
|
||||||
/* Materials */
|
/* Materials */
|
||||||
@ -149,4 +152,25 @@ bool USDAbstractWriter::mark_as_instance(const HierarchyContext &context, const
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void USDAbstractWriter::author_extent(const pxr::UsdTimeCode timecode, pxr::UsdGeomBoundable &prim)
|
||||||
|
{
|
||||||
|
/* Do not use any existing `extentsHint` that may be authored, instead recompute the extent when
|
||||||
|
* authoring it. */
|
||||||
|
const bool useExtentsHint = false;
|
||||||
|
const pxr::TfTokenVector includedPurposes{pxr::UsdGeomTokens->default_};
|
||||||
|
pxr::UsdGeomBBoxCache bboxCache(timecode, includedPurposes, useExtentsHint);
|
||||||
|
pxr::GfBBox3d bounds = bboxCache.ComputeLocalBound(prim.GetPrim());
|
||||||
|
if (pxr::GfBBox3d() == bounds) {
|
||||||
|
/* This will occur, for example, if a mesh does not have any vertices. */
|
||||||
|
WM_reportf(RPT_WARNING,
|
||||||
|
"USD Export: no bounds could be computed for %s",
|
||||||
|
prim.GetPrim().GetName().GetText());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pxr::VtArray<pxr::GfVec3f> extent{(pxr::GfVec3f)bounds.GetRange().GetMin(),
|
||||||
|
(pxr::GfVec3f)bounds.GetRange().GetMax()};
|
||||||
|
prim.CreateExtentAttr().Set(extent);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace blender::io::usd
|
} // namespace blender::io::usd
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <pxr/usd/sdf/path.h>
|
#include <pxr/usd/sdf/path.h>
|
||||||
#include <pxr/usd/usd/stage.h>
|
#include <pxr/usd/usd/stage.h>
|
||||||
|
#include <pxr/usd/usdGeom/boundable.h>
|
||||||
#include <pxr/usd/usdShade/material.h>
|
#include <pxr/usd/usdShade/material.h>
|
||||||
#include <pxr/usd/usdUtils/sparseValueWriter.h>
|
#include <pxr/usd/usdUtils/sparseValueWriter.h>
|
||||||
|
|
||||||
@ -67,6 +68,24 @@ class USDAbstractWriter : public AbstractHierarchyWriter {
|
|||||||
* Reference the original data instead of writing a copy.
|
* Reference the original data instead of writing a copy.
|
||||||
*/
|
*/
|
||||||
virtual bool mark_as_instance(const HierarchyContext &context, const pxr::UsdPrim &prim);
|
virtual bool mark_as_instance(const HierarchyContext &context, const pxr::UsdPrim &prim);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the bounds for a boundable prim, and author the result as the `extent` attribute.
|
||||||
|
*
|
||||||
|
* Although this method works for any boundable prim, it is preferred to use Blender's own
|
||||||
|
* cached bounds when possible.
|
||||||
|
*
|
||||||
|
* This method does not author the `extentsHint` attribute, which is also important to provide.
|
||||||
|
* Whereas the `extent` attribute can only be authored on prims inheriting from
|
||||||
|
* `UsdGeomBoundable`, an `extentsHint` can be provided on any prim, including scopes. This
|
||||||
|
* `extentsHint` should be authored on every prim in a hierarchy being exported.
|
||||||
|
*
|
||||||
|
* Note that this hint is only useful when importing or inspecting layers, and should not be
|
||||||
|
* taken into account when computing extents during export.
|
||||||
|
*
|
||||||
|
* TODO: also provide method for authoring extentsHint on every prim in a hierarchy.
|
||||||
|
*/
|
||||||
|
virtual void author_extent(const pxr::UsdTimeCode timecode, pxr::UsdGeomBoundable &prim);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace blender::io::usd
|
} // namespace blender::io::usd
|
||||||
|
@ -62,6 +62,8 @@ void USDHairWriter::do_write(HierarchyContext &context)
|
|||||||
colors.push_back(pxr::GfVec3f(cache[0]->col));
|
colors.push_back(pxr::GfVec3f(cache[0]->col));
|
||||||
curves.CreateDisplayColorAttr(pxr::VtValue(colors));
|
curves.CreateDisplayColorAttr(pxr::VtValue(colors));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this->author_extent(timecode, curves);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool USDHairWriter::check_is_animated(const HierarchyContext & /*context*/) const
|
bool USDHairWriter::check_is_animated(const HierarchyContext & /*context*/) const
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
#include "usd_writer_mesh.h"
|
#include "usd_writer_mesh.h"
|
||||||
#include "usd_hierarchy_iterator.h"
|
#include "usd_hierarchy_iterator.h"
|
||||||
|
|
||||||
|
#include <pxr/usd/usdGeom/bboxCache.h>
|
||||||
#include <pxr/usd/usdGeom/mesh.h>
|
#include <pxr/usd/usdGeom/mesh.h>
|
||||||
#include <pxr/usd/usdGeom/primvarsAPI.h>
|
#include <pxr/usd/usdGeom/primvarsAPI.h>
|
||||||
#include <pxr/usd/usdShade/material.h>
|
#include <pxr/usd/usdShade/material.h>
|
||||||
@ -247,6 +248,15 @@ void USDGenericMeshWriter::write_mesh(HierarchyContext &context, Mesh *mesh)
|
|||||||
if (usd_export_context_.export_params.export_materials) {
|
if (usd_export_context_.export_params.export_materials) {
|
||||||
assign_materials(context, usd_mesh, usd_mesh_data.face_groups);
|
assign_materials(context, usd_mesh, usd_mesh_data.face_groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Blender grows its bounds cache to cover animated meshes, so only author once. */
|
||||||
|
float bound_min[3];
|
||||||
|
float bound_max[3];
|
||||||
|
INIT_MINMAX(bound_min, bound_max);
|
||||||
|
BKE_mesh_minmax(mesh, bound_min, bound_max);
|
||||||
|
pxr::VtArray<pxr::GfVec3f> extent{pxr::GfVec3f{bound_min[0], bound_min[1], bound_min[2]},
|
||||||
|
pxr::GfVec3f{bound_max[0], bound_max[1], bound_max[2]}};
|
||||||
|
usd_mesh.CreateExtentAttr().Set(extent);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void get_vertices(const Mesh *mesh, USDMeshData &usd_mesh_data)
|
static void get_vertices(const Mesh *mesh, USDMeshData &usd_mesh_data)
|
||||||
|
@ -910,6 +910,12 @@ if(WITH_ALEMBIC)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(WITH_USD)
|
if(WITH_USD)
|
||||||
|
add_blender_test(
|
||||||
|
usd_export_test
|
||||||
|
--python ${CMAKE_CURRENT_LIST_DIR}/bl_usd_export_test.py
|
||||||
|
--
|
||||||
|
--testdir "${TEST_SRC_DIR}/usd"
|
||||||
|
)
|
||||||
add_blender_test(
|
add_blender_test(
|
||||||
usd_import_test
|
usd_import_test
|
||||||
--python ${CMAKE_CURRENT_LIST_DIR}/bl_usd_import_test.py
|
--python ${CMAKE_CURRENT_LIST_DIR}/bl_usd_import_test.py
|
||||||
|
144
tests/python/bl_usd_export_test.py
Normal file
144
tests/python/bl_usd_export_test.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
import enum
|
||||||
|
import pathlib
|
||||||
|
import pprint
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pxr import Usd
|
||||||
|
from pxr import UsdUtils
|
||||||
|
from pxr import UsdGeom
|
||||||
|
from pxr import Gf
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
args = None
|
||||||
|
|
||||||
|
class AbstractUSDTest(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls._tempdir = tempfile.TemporaryDirectory()
|
||||||
|
cls.testdir = args.testdir
|
||||||
|
cls.tempdir = pathlib.Path(cls._tempdir.name)
|
||||||
|
|
||||||
|
return cls
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.assertTrue(
|
||||||
|
self.testdir.exists(), "Test dir {0} should exist".format(self.testdir)
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self._tempdir.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
class USDExportTest(AbstractUSDTest):
|
||||||
|
def test_export_usdchecker(self):
|
||||||
|
"""Test exporting a scene and verifying it passes the usdchecker test suite"""
|
||||||
|
bpy.ops.wm.open_mainfile(
|
||||||
|
filepath=str(self.testdir / "usd_materials_export.blend")
|
||||||
|
)
|
||||||
|
export_path = self.tempdir / "usdchecker.usda"
|
||||||
|
res = bpy.ops.wm.usd_export(
|
||||||
|
filepath=str(export_path),
|
||||||
|
export_materials=True,
|
||||||
|
evaluation_mode="RENDER",
|
||||||
|
)
|
||||||
|
self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}")
|
||||||
|
|
||||||
|
checker = UsdUtils.ComplianceChecker(
|
||||||
|
arkit=False,
|
||||||
|
skipARKitRootLayerCheck=False,
|
||||||
|
rootPackageOnly=False,
|
||||||
|
skipVariants=False,
|
||||||
|
verbose=False,
|
||||||
|
)
|
||||||
|
checker.CheckCompliance(str(export_path))
|
||||||
|
|
||||||
|
failed_checks = {}
|
||||||
|
|
||||||
|
# The ComplianceChecker does not know how to resolve <UDIM> tags, so
|
||||||
|
# it will flag "textures/test_grid_<UDIM>.png" as a missing reference.
|
||||||
|
# That reference is in fact OK, so we skip the rule for this test.
|
||||||
|
to_skip = ("MissingReferenceChecker",)
|
||||||
|
for rule in checker._rules:
|
||||||
|
name = rule.__class__.__name__
|
||||||
|
if name in to_skip:
|
||||||
|
continue
|
||||||
|
|
||||||
|
issues = rule.GetFailedChecks() + rule.GetWarnings() + rule.GetErrors()
|
||||||
|
if not issues:
|
||||||
|
continue
|
||||||
|
|
||||||
|
failed_checks[name] = issues
|
||||||
|
|
||||||
|
self.assertFalse(failed_checks, pprint.pformat(failed_checks))
|
||||||
|
|
||||||
|
def compareVec3d(self, first, second):
|
||||||
|
places = 5
|
||||||
|
self.assertAlmostEqual(first[0], second[0], places)
|
||||||
|
self.assertAlmostEqual(first[1], second[1], places)
|
||||||
|
self.assertAlmostEqual(first[2], second[2], places)
|
||||||
|
|
||||||
|
def test_export_extents(self):
|
||||||
|
"""Test that exported scenes contain have a properly authored extent attribute on each boundable prim"""
|
||||||
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_extent_test.blend"))
|
||||||
|
export_path = self.tempdir / "usd_extent_test.usda"
|
||||||
|
res = bpy.ops.wm.usd_export(
|
||||||
|
filepath=str(export_path),
|
||||||
|
export_materials=True,
|
||||||
|
evaluation_mode="RENDER",
|
||||||
|
)
|
||||||
|
self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}")
|
||||||
|
|
||||||
|
# if prims are missing, the exporter must have skipped some objects
|
||||||
|
stats = UsdUtils.ComputeUsdStageStats(str(export_path))
|
||||||
|
self.assertEqual(stats["totalPrimCount"], 15, "Unexpected number of prims")
|
||||||
|
|
||||||
|
# validate the overall world bounds of the scene
|
||||||
|
stage = Usd.Stage.Open(str(export_path))
|
||||||
|
scenePrim = stage.GetPrimAtPath("/scene")
|
||||||
|
bboxcache = UsdGeom.BBoxCache(Usd.TimeCode.Default(), [UsdGeom.Tokens.default_])
|
||||||
|
bounds = bboxcache.ComputeWorldBound(scenePrim)
|
||||||
|
bound_min = bounds.GetRange().GetMin()
|
||||||
|
bound_max = bounds.GetRange().GetMax()
|
||||||
|
self.compareVec3d(bound_min, Gf.Vec3d(-5.752975881, -1, -2.798513651))
|
||||||
|
self.compareVec3d(bound_max, Gf.Vec3d(1, 2.9515805244, 2.7985136508))
|
||||||
|
|
||||||
|
# validate the locally authored extents
|
||||||
|
prim = stage.GetPrimAtPath("/scene/BigCube/BigCubeMesh")
|
||||||
|
extent = UsdGeom.Boundable(prim).GetExtentAttr().Get()
|
||||||
|
self.compareVec3d(Gf.Vec3d(extent[0]), Gf.Vec3d(-1, -1, -2.7985137))
|
||||||
|
self.compareVec3d(Gf.Vec3d(extent[1]), Gf.Vec3d(1, 1, 2.7985137))
|
||||||
|
prim = stage.GetPrimAtPath("/scene/LittleCube/LittleCubeMesh")
|
||||||
|
extent = UsdGeom.Boundable(prim).GetExtentAttr().Get()
|
||||||
|
self.compareVec3d(Gf.Vec3d(extent[0]), Gf.Vec3d(-1, -1, -1))
|
||||||
|
self.compareVec3d(Gf.Vec3d(extent[1]), Gf.Vec3d(1, 1, 1))
|
||||||
|
prim = stage.GetPrimAtPath("/scene/Volume/Volume")
|
||||||
|
extent = UsdGeom.Boundable(prim).GetExtentAttr().Get()
|
||||||
|
self.compareVec3d(
|
||||||
|
Gf.Vec3d(extent[0]), Gf.Vec3d(-0.7313742, -0.68043584, -0.5801515)
|
||||||
|
)
|
||||||
|
self.compareVec3d(
|
||||||
|
Gf.Vec3d(extent[1]), Gf.Vec3d(0.7515701, 0.5500924, 0.9027928)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global args
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
if "--" in sys.argv:
|
||||||
|
argv = [sys.argv[0]] + sys.argv[sys.argv.index("--") + 1:]
|
||||||
|
else:
|
||||||
|
argv = sys.argv
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--testdir", required=True, type=pathlib.Path)
|
||||||
|
args, remaining = parser.parse_known_args(argv)
|
||||||
|
|
||||||
|
unittest.main(argv=remaining)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Loading…
Reference in New Issue
Block a user