Generate procedural hair cards from hair strands
https://dev.epicgames.com/community/learning/tutorials/Kp2e/unreal-engine-metahuman-hair-card-generator
I dont know why but in latest Version 5.4 when clicking the button nothing is happening.
Check the logs in editor after clicking the button to see if there’s an error, if you post logs for a run where you pressed the button I will also take a look.
Mark Resolved my issue, if anyone here having issues aswell make sure to check if your c:/ drive does have enough disk space available to download python libraries the Generator needs.
~4 gigs id opt in for 10 haha.
Refresh the window for better performance
Was you issue having this log?
LogHairCardGenerator: Error: Failed to find an active hair card controller.
LogHairCardGenerator: Error: No hair card generator controller found, cannot generate hair cards!
How did you download the python library?
Hi. I was looking for this type of tool for a long time .
So I tried to convert the hair that i created in Blender with the use of curve hair toool and exported is as .abc.
Firstly I tired to convert a part of fur , and after clicking ,Generete ‘’ button unreal crushed.
Fatal error: [File:D:\build++UE5\Sync\Engine\Source\Runtime\Core\Private\Containers\ContainerHelpers.cpp] [Line: 8] Trying to resize TArray to an invalid size of 2147483648
So I went with something less complex. UE did not crush but returned error:
LogPython: [StrandCardAllocator] Feature generation…
LogPython: [StrandCardAllocator] Clustering features…
LogScript: Error: Script Msg: Traceback (most recent call last):
File “C:\Program Files/Epic Games/UE_5.4/Engine/Plugins/Experimental/HairCardGenerator/Content/Python\CardUEInterop\ue_card_gen_controller.py”, line 174, in generate_clumps
clump_gen.cluster_strands(target_num_clumps=group_settings.num_clumps,
File “C:\Program Files/Epic Games/UE_5.4/Engine/Plugins/Experimental/HairCardGenerator/Content/Python\Modules\Geometry\cluster\clump_generator.py”, line 62, in cluster_strands
labels_local, max_main_clump = self._strand_allocator.allocate(target_num_clumps,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File “C:\Program Files/Epic Games/UE_5.4/Engine/Plugins/Experimental/HairCardGenerator/Content/Python\Modules\Geometry\cluster\strand_card_allocation.py”, line 147, in allocate
ind_closest_strand = pairwise_distances(point_features[mask], point_features[np.logical_not(mask)]).argmin(axis=-1)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File “C:\Users\macie\Documents\Unreal Projects\Friend\Intermediate\PipInstall\Lib\site-packages\sklearn\metrics\pairwise.py”, line 2039, in pairwise_distances
return _parallel_pairwise(X, Y, func, n_jobs, **kwds)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File “C:\Users\macie\Documents\Unreal Projects\Friend\Intermediate\PipInstall\Lib\site-packages\sklearn\metrics\pairwise.py”, line 1579, in _parallel_pairwise
return func(X, Y, **kwds)
^^^^^^^^^^^^^^^^^^
File “C:\Users\macie\Documents\Unreal Projects\Friend\Intermediate\PipInstall\Lib\site-packages\sklearn\metrics\pairwise.py”, line 300, in euclidean_distances
X, Y = check_pairwise_arrays(X, Y)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File “C:\Users\macie\Documents\Unreal Projects\Friend\Intermediate\PipInstall\Lib\site-packages\sklearn\metrics\pairwise.py”, line 163, in check_pairwise_arrays
Y = check_array(
^^^^^^^^^^^^
File “C:\Users\macie\Documents\Unreal Projects\Friend\Intermediate\PipInstall\Lib\site-packages\sklearn\utils\validation.py”, line 931, in check_array
raise ValueError(
ValueError: Found array with 0 sample(s) (shape=(0, 24)) while a minimum of 1 is required by check_pairwise_arrays.
LogScript: Error: Script call stack:
/Engine/PythonTypes.HairCardGenController.GenerateClumps
LogHairCardGenerator: Error: Strand clustering failed for settings group 0. Check the log for details.
LogHairCardGenerator: Error: Failed to generate cards for all groups. See log for details.
I’m working with 5.4.4 version of Unreal Eninge
I am struggling with exporting Group_id from Maya.
I was able to attach groom id normally and it works, but group id doesnt export, I think(?) . nothing show up in the card generator? any tips ?
This error is almost always caused by the python dependencies failing to download. Check the logs (search for LogPython) and see if there’s an error. There should be some messages early on about pip installing dependencies, and there will likely be an error right around there.
I can also take a look if you post the logs.
I am looking in to this issue, but in the meantime, in 5.5 we slightly updated this part of the hair card generator to support much larger number of strands by disabling the flyaway check:
If you run with number of flyaways set to 0, it should avoid that section and also run much faster. Unfortunately you’ll have to update to 5.5 to try it out.
I’m unfamiliar with maya’s alembic export setup, so I might not be much help here. But in order to appear in the card generator group list, you should name the property groom_groups_card_id
, it should be a character string, and it should be a per-strand property. If all of that is true, you should see the set of group names for all strands listed in the generator dialog.
(post deleted by author)
Hi and thanks for the reply.
I followed the tutorial to a T, if my “calculations” are correct, but still it did not seem to be able to export or work, while groom_id
(for strand based groups) did work and I saw 2 groups, I then adjusted my script for groom_groups_card_id,
and it was added the same, should have worked, but nothing happens when it goes into unreal for cards.
I was so desprate I tried Houdini, but I am not familiar enough with it to achieve it and tutorials aren’t easily available for this workflow.
How do you go about it, Blender/Houdini?
Hi,
It turns out that our tutorial documentation has the incorrect name for the property it should be groom_group_cards_id
. I will modify the tutorial as soon as I get permission, but in the meantime, try changing the export name and see if that works.
If this is still giving you problems, perhaps you can post a test alembic file here (doesn’t need to be a real asset just something with curve properties exported in the same way), and I’ll take a look as soon as I’m able.
Sorry for the confusion, this was likely my oversight in the original tutorial write-up.
-Mark Winter
Hi Mark,
Thanks for the help.
Unfortunately I still can’t get it to work even with the new value.
The normal Groom_Group_ID, works perfectly fine. but when trying the same with adding “cards” nothing. So I am quiet lost at what could be the issue as the workflow suggest to use the workflow which works (i’ve tried many different combinations, using only the card_ids value, as far as modifying script script to assign value to each curve separately).
Perhaps this new method isnt tested in Maya yet? Could you mention how you got it to work ?
I have uploaded an ABC test file + pre export images + my script.
Really hoping this helps find a solution.
from maya import cmds
attr_name = 'groom_group_cards_id'
# NOTE: change the following names to reflect your node's scene.
groups = ['description1|SplineGrp10', 'description2|SplineGrp20']
for groom_group_id, group_name in enumerate(groups):
# get curves under xgGroom
curves = cmds.listRelatives(group_name, ad=True, type='nurbsCurve')
# tag group with group id
cmds.addAttr(group_name, longName=attr_name, attributeType='short', defaultValue=groom_group_id, keyable=True)
# add attribute scope
# forces Maya's alembic to export data as GeometryScope::kConstantScope
cmds.addAttr(group_name, longName='{}_AbcGeomScope'.format(attr_name), dataType='string', keyable=True)
cmds.setAttr('{}.{}_AbcGeomScope'.format(group_name, attr_name), 'con', type='string')```
Hi, the guide mentions “It is recommended to generate a card asset using a couple of different random seeds.” It would be nice if we had a way to batch process conversions with different settings so we could run it overnight on a large number of hairs. Would something like this be possible via editor scripting or C++?
I solved this for myself. I was missing a bunch of Python dependencies. I ran the following commands as admin into command prompt & restarted the editor. If this doesn’t work, keep checking the output log for errors.
"C:\Program Files\Epic Games\UE_5.4\Engine\Binaries\ThirdParty\Python3\Win64\python.exe" -m pip install numpy==1.23.5 scipy pyyaml addict torch scikit-learn moderngl Pillow pyrr PyOpenGL --target="C:\Program Files\Epic Games\UE_5.4\Engine\Binaries\ThirdParty\Python3\Win64\Lib\site-packages" --upgrade
Hello @Guy7,
I apologize for the delay, unfortunately we don’t use a maya pipeline for hair internally, and it seems our Houdini setup for this feature isn’t stable either. One of our technical animators was able to build off of your code snippet and export something that’ll set up the property appropriately in the alembic. I’ve quoted their response below:
This is the modified snippet that should add the haircard group attribute as “string” type. It adds the attribute to both, the transform node and each of the shape curves contained within. It also disables the
riCurves
attribute.And this is the command to export the ABC file, theattr
flag should contain all the attributes that need to be exported space separated, in this case we are just exportinggroom_group_cards_id
attribute:
import maya.cmds as cmds
cmds.AbcExport(j='-frameRange 0 0 -attr groom_group_cards_id -root grpone|splineone -root grptwo|splinetwo -file C:/abcstuff/Test_Splines.abc')
And below is the script they used to add the property:
from maya import cmds
attr_name = 'groom_group_cards_id'
# NOTE: change the following names to reflect your node's scene.
groups = ['grpone|splineone', 'grptwo|splinetwo']
for groom_group_id, group_name in enumerate(groups):
# get curves under xgGroom
curves = cmds.listRelatives(group_name, ad=True, type='nurbsCurve')
# tag group with group id
if not cmds.objExists('{}.{}'.format(group_name, attr_name)):
cmds.addAttr(group_name, longName=attr_name, dataType='string', keyable=True)
cmds.setAttr('{}.{}'.format(group_name, attr_name), '{}'.format(groom_group_id), type='string')
# add attribute scope
# forces Maya's alembic to export data as GeometryScope::kUniformScope
if not cmds.objExists('{}.{}_AbcGeomScope'.format(group_name, attr_name)):
cmds.addAttr(group_name, longName='{}_AbcGeomScope'.format(attr_name), dataType='string', keyable=True)
cmds.setAttr('{}.{}_AbcGeomScope'.format(group_name, attr_name), 'con', type='string')
for crv in curves:
# tag group with group id
if not cmds.objExists('{}.{}'.format(crv, attr_name)):
cmds.addAttr(crv, longName=attr_name, dataType='string', keyable=True)
cmds.setAttr('{}.{}'.format(crv, attr_name), '{}'.format(groom_group_id), type='string')
# add attribute scope
# forces Maya's alembic to export data as GeometryScope::kUniformScope
if not cmds.objExists('{}.{}_AbcGeomScope'.format(crv, attr_name)):
cmds.addAttr(crv, longName='{}_AbcGeomScope'.format(attr_name), dataType='string', keyable=True)
cmds.setAttr('{}.{}_AbcGeomScope'.format(crv, attr_name), 'con', type='string')
Their comment on this solution:
So, seems that we need to add the attribute on each of the curve shapes and ensure it is exported to ABC either without the
riCurves
attribute present or set toFalse
.
I was able to load and verify that the groups appeared in the generator tool when using the abc file generated with this technique. We messed around with several other techniques, but no other method generated the property (as a string) per-curve in the alembic.
If you have a Houdini install they have some standalone utilities for checking alembic files, abcecho will show you the structure of the scene with attributes and types so you can verify the groom_group_cards_id
attribute.
-Mark Winter
This method will work, but has some downsides:
- The library versions won’t match those expected/tested internally, and may not be an exhaustive list of requirements for all enabled plugins
- The libraries will be installed in a different location than where the autoinstall places them
The first point, in particular can slow down editor startup times, as the installer might still attempt to download and install some other libraries (and keep failing). The second point should only be an issue if you use the same engine install for multiple projects with different plugins enabled, since they may have different python dependencies.
I would still recommend using the logs to troubleshoot the internal pip install issue, but here is how to avoid the issues I mentioned above:
- Disable the internal pip installer by unchecking the
Run Pip Install on Startup
option under Project Setttings → Plugins → Python - Delete the
<ProjectDir>/Intermediate/PipInstall
directory - Run the editor once and then grab
<ProjectDir>/Intermediate/PipInstall/merged_requirements.in
- Install all dependencies listed in
merged_requirements.in
usingpip install -r
or manually listing them - [Optional] You can also change/remove versions numbers for libraries in the merged list if you want to get the latest, we simply won’t have tested plugins against those versions
Note: For step 4 above packages can be installed to the global engine site-packages
as you’ve done in your post, or to the “project” site-packages dir <ProjectDir>/Intermediate/PipInstall/Lib/site-packages
(Windows).
The internal pip install is documented here and has a bit more detail about how this works.
-Mark Winter