Fix #148436: Incorrect legacy hair offset with USD export

The report in question uncovered two sources of issues. The primary one
being that there was an accidental double-transform from placing the
basis curves prim under the main Xform of the mesh too. This is solved
by considering the inverse of mesh's world transform when writing out
the curve points.

The second was exposed with the new, more correct, viewport drawing of
curves that showed that we were exporting the wrong curve type. This
would manifest as a disappearing curve segment at the beginning and end
of the curve. Fixed by explicitly writing out catmull-rom, pinned,
curves for the hair rather than using bsplines.

As a result the Storm-USD tests now match much closer to the native
Storm-Hydra variants.

Pull Request: https://projects.blender.org/blender/blender/pulls/148543
This commit is contained in:
Jesse Yurkovich 2025-10-22 22:26:28 +02:00 • committed by Jesse Yurkovich
parent 6cbbb54d8c
commit 67752cc66e
15 changed files with 52 additions and 38 deletions

View file

@ -9,6 +9,9 @@
#include "BKE_particle.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_particle_types.h"
namespace blender::io::usd {
@ -27,14 +30,24 @@ void USDHairWriter::do_write(HierarchyContext &context)
pxr::UsdGeomBasisCurves curves = pxr::UsdGeomBasisCurves::Define(usd_export_context_.stage,
usd_export_context_.usd_path);
/* TODO(Sybren): deal with (psys->part->flag & PART_HAIR_BSPLINE) */
curves.CreateBasisAttr(pxr::VtValue(pxr::UsdGeomTokens->bspline));
curves.CreateTypeAttr(pxr::VtValue(pxr::UsdGeomTokens->cubic));
if (psys->part->flag & PART_HAIR_BSPLINE) {
curves.CreateBasisAttr(pxr::VtValue(pxr::UsdGeomTokens->bspline));
curves.CreateTypeAttr(pxr::VtValue(pxr::UsdGeomTokens->cubic));
}
else {
curves.CreateBasisAttr(pxr::VtValue(pxr::UsdGeomTokens->catmullRom));
curves.CreateTypeAttr(pxr::VtValue(pxr::UsdGeomTokens->linear));
curves.CreateWrapAttr(pxr::VtValue(pxr::UsdGeomTokens->pinned));
}
pxr::VtArray<pxr::GfVec3f> points;
pxr::VtIntArray curve_point_counts;
curve_point_counts.reserve(psys->totpart);
/* Reverse current transform since the Hair curves will be placed under the object's Xform and we
* don't want a double-transform to happen. */
const blender::float4x4 inv = blender::math::invert(context.object->object_to_world());
ParticleCacheKey *strand;
for (int strand_index = 0; strand_index < psys->totpart; ++strand_index) {
strand = cache[strand_index];
@ -43,7 +56,8 @@ void USDHairWriter::do_write(HierarchyContext &context)
curve_point_counts.push_back(point_count);
for (int point_index = 0; point_index < point_count; ++point_index, ++strand) {
points.push_back(pxr::GfVec3f(strand->co));
const float3 vert = blender::math::transform_point(inv, float3(strand->co));
points.push_back(pxr::GfVec3f(vert.x, vert.y, vert.z));
}
}

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:056f05c238c5a35e467c8e1b5df635f58a8e47276911b14879bb13458f74621c
size 1448
oid sha256:18bb866d0483ad3a2af070da1e75ed0a931088845998088c5a45ccfe967613f0
size 1487

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:78a994390987ae71d122f711a059da923a8ebc2754b350c06341566a75036c7e
size 933
oid sha256:80eb07336eec80945fa6071272fb5ed89ee2c48b74543be3848312808141f544
size 1683

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:056b08ea90ba933b7e08c5f646950f33ab0abfc8e4fdddd1377322c788400030
size 955
oid sha256:7e1c54c34f9545cc549827cd67941c50d6de73743bb964940a70ca8e74c02dbf
size 3111

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2e006d081384e4d3e518c3090505508d95f136328e8454d056c847899346306f
size 6578
oid sha256:fa4a518c50d13a82057cc8a2387bbb54c128c5e5207faea26ae95c7a8438099c
size 11570

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a893f9b4f70c24a568a288d8151bacd545c4bb72b9fd1afecf21e0197b372001
size 885
oid sha256:7fc7e76a1b167871f4ea7380377b5c52ae09af511b76fe0bf7e9a6ed8f096830
size 1690

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dfc90d81a399f3dcc357c94ec14a4715b650cc384dac4649e269578d5fe1bfb3
size 6282
oid sha256:a88409c33674d8ea30bb3cb8a0680c374bd490ff031acaa949f93cffd365e98e
size 10825

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e25037087446e2ff1f3bff7b606e20d38342ccc7caef974a37331bc39f6383dc
size 940
oid sha256:b53689af07cc1c6ac1ebc33994ac4e4c743d9ad2fae0085017e00dc9ce9d0735
size 1690

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4c3b4a7f6761c5a8c3f052c75f30e54db3e8e53b6ee9fc0b5e903c651ff0c21f
size 1846
oid sha256:529536ecce9652642af712b98c254a443afb4f22d1d9a28e4089fbff2f4dbe2b
size 1710

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c6b28fa6587d713cd551672da0e5b6c3b77759af507cc3a0543cd5ab9214ff79
size 1839
oid sha256:50c2776c7dac9cab387756dd236e0e69c53e2b62e879d832d765b32925533930
size 1703

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a861c9af24dbdbc834f1eca830c9b470e44eb33412e22d32b083674cf30fcdee
size 1845
oid sha256:b936db41b5e749f62c2d9bffc5a1800b270c789b548f25e16c1e967b1aa244c5
size 1709

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:28939bf8869635716ce2014697640915ef4d46e73bc19e8fe08026461f0a4da2
size 2195
oid sha256:188ebf7e81de30973fdfb6249ff0a666b2455f279bf8a840b2feb497544c19e2
size 2939

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0ac731b35867f91e32a811e985f20b40227238e8ef2c075ff136530469da8cde
size 9959
oid sha256:5f689c8ad59f3e4670db79502f7e7b6b26f9da15ac00adce64dc79f09bff75eb
size 9950

View file

@ -535,8 +535,8 @@ class USDExportTest(AbstractUSDTest):
hair_curves = UsdGeom.BasisCurves(hair_prim)
hair_samples = hair_curves.GetPointsAttr().GetTimeSamples()
self.assertEqual(hair_curves.GetTypeAttr().Get(), "cubic")
self.assertEqual(hair_curves.GetBasisAttr().Get(), "bspline")
self.assertEqual(hair_curves.GetTypeAttr().Get(), "linear")
self.assertEqual(hair_curves.GetBasisAttr().Get(), "catmullRom")
self.assertEqual(len(hair_samples), 10)
def check_primvar(self, prim, pv_name, pv_typeName, pv_interp, elements_len):

View file

@ -904,8 +904,8 @@ class USDImportTest(AbstractUSDTest):
self.assertAlmostEqual(f.evaluate(10), 0.0, 2, "Unexpected value for rotation quaternion Z curve at frame 10")
def check_curve(self, blender_curve, usd_curve):
curve_type_map = {"linear": 1, "cubic-bezier": 2, "cubic-bspline": 3}
cyclic_map = {"nonperiodic": False, "periodic": True}
curve_type_map = {"linear-bezier": 1, "linear-catmullRom": 1, "cubic-bezier": 2, "cubic-bspline": 3}
cyclic_map = {"pinned": False, "nonperiodic": False, "periodic": True}
# Check correct spline count.
blender_spline_count = len(blender_curve.attributes["curve_type"].data)
@ -913,10 +913,7 @@ class USDImportTest(AbstractUSDTest):
self.assertEqual(blender_spline_count, usd_spline_count)
# Check correct type of curve. All splines should have the same type and periodicity.
usd_curve_type = usd_curve.GetTypeAttr().Get()
usd_curve_type_basis = usd_curve_type
if usd_curve_type != "linear":
usd_curve_type_basis = usd_curve_type + "-" + usd_curve.GetBasisAttr().Get()
usd_curve_type_basis = usd_curve.GetTypeAttr().Get() + "-" + usd_curve.GetBasisAttr().Get()
usd_cyclic = usd_curve.GetWrapAttr().Get()
expected_curve_type = curve_type_map[usd_curve_type_basis]
expected_cyclic = cyclic_map[usd_cyclic]
@ -935,7 +932,7 @@ class USDImportTest(AbstractUSDTest):
blender_positions = blender_curve.attributes["position"].data
point_count = 0
if usd_curve_type_basis == "linear":
if usd_curve_type_basis in ("linear-bezier", "linear-catmullRom"):
point_count = len(usd_positions)
self.assertEqual(len(blender_positions), point_count)
elif usd_curve_type_basis == "cubic-bezier":
@ -957,10 +954,13 @@ class USDImportTest(AbstractUSDTest):
if usd_curve_type_basis == "cubic-bspline":
return
if not usd_curve.GetWidthsAttr().IsAuthored():
return
usd_width_interpolation = usd_curve.GetWidthsInterpolation()
usd_radius = [w / 2 for w in usd_curve.GetWidthsAttr().Get()]
blender_radius = [r.value for r in blender_curve.attributes["radius"].data]
if usd_curve_type_basis == "linear":
if usd_curve_type_basis == "linear-bezier":
if usd_width_interpolation == "constant":
usd_radius = usd_radius * point_count