XGen Guidelines for Hair Creation

Describes guidelines for exporting grooms as Alembic files for use with Unreal Engine.


This guide will show you how to set up a groom for import from Maya's legacy XGen hair creation system for importing into Unreal Engine with a set of supported attributes outlined in the Alembic for Grooms Specification documentation.

Maya 2018.6 was used in creation of assets for this guide.

Converting Legacy XGen Descriptions

Convert Guides to NURBS Curves

Use the following steps to convert your groom's guides to curves in order to save out a set of curves that match the guides you want to transfer.

  1. Set Maya's menu set to Modeling so that you see the correct menu options available.


  2. From the main menu, click the Generate dropdown and select XGen Editor.

  3. In the XGen window, use the Utilities tab to select Guides to Curves.


  4. Click Create Curves.

Once completed, your groom's output should look similar to this:


Convert Grooms to XGen Interactive Grooms

If you're using legacy XGen Description, the groom will need to be converted to be an XGen Interactive Groom. You can do this by:

  1. Select the XGen Description nodes.


  2. While in the Modeling menu set, use the main menu to click the Generate dropdown, then select Convert to Interactive Groom.


Exporting Spline Descriptions to NURBS Curves

Follow these steps to export your selected spline descriptions as an Alembic file that can be imported with the interpolated hairs as NURBS curves.

  1. Select your XGen Spline Description nodes, and while in the Modeling menu set, use the main menu to click the Generate dropdown. Select Cache > Export Cache from the list.


  2. In the Export Cache window, set the following:

    • Cache Time Frame: Set to Current Frame

    • Multiple Transforms: Disabled

    • Write Final Width: Enabled


  3. Enter a name for your file, and select Alembic as the file type.


  4. Click Export.

  5. Use the File menu to select Import. This opens the Import window, which can be used to select and import your Alembic ('.abc') file into your scene.


Once imported, you should now have an XGen Spline Description that was exported and saved as an Alembic file and imported bringing in the interpolated hairs in as NURBS curves.


Creating Attributes

Create Group ID Attributes

Interpolated hair can be exported in one or more groups. These groups are recognized in Unreal Engine for unique material assignment.

Use the following script when creating group ID attributes:

from maya import cmds

attr_name = 'groom_group_id'

# NOTE: change the following names to reflect your node's scene.
groups = ['hair_brows_splineDescription1|SplineGrp0', 'hair_lashes_splineDescription1|SplineGrp0', 'hair_head_splineDescription1|SplineGrp0']

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')

Create Guide Attributes

When creating the guide attributes for your groom, only the curves tagged as guide are used for simulation in Unreal Engine. If there are no guides specified in the Alembic file, a percentage of the interpolated hairs will be internally tagged as guides during the import process into UE4.

When importing a groom with no guides, the percentage of interpolated hairs that are tagged as guides can be set using the Groom Import Options . By default, only 10% of the number of hairs will be used as guides.

Use the following script when creating your guide attributes:

from maya import cmds

attr_name = 'groom_guide'

# get curves under xgGroom
curves = cmds.listRelatives('xgGroom', ad=True, type='nurbsCurve')

# create new group
guides_group = cmds.createNode('transform', name='guides')

# tag group as groom_guide
cmds.addAttr(guides_group, longName=attr_name, attributeType='short', defaultValue=1, keyable=True)

# forces Maya's alembic to export curves as one group.
cmds.addAttr(guides_group, longName='riCurves', attributeType='bool', defaultValue=1, keyable=True)

# add attribute scope
# forces Maya's alembic to export data as GeometryScope::kConstantScope
cmds.addAttr(guides_group, longName='{}_AbcGeomScope'.format(attr_name), dataType='string', keyable=True)
cmds.setAttr('{}.{}_AbcGeomScope'.format(guides_group, attr_name), 'con', type='string')

# parent curves under guides group
for curve in curves:
    cmds.parent(curve, guides_group, shape=True, relative=True)

Groom_Width Attribute

For Maya, the width value has a special behavior, unlike other DCC applications that can follow the Alembic for Grooms Specification to retrieve them and use them to build the groom.

Maya can export width values directly on the curves so that there's no need to export a custom groom_width attribute; the importer converts those values into that attribute automatically. Should the groom_wdith attribute be present with the groom during import into Unreal Engine, it is not overwritten. If groom_wdith is not specified, or cannot be converted from width values, the builder will fal back on a value of 1 centimeter.

Export to Alembic from Maya

  1. In Maya, select the Guides and Group_ID curves that you want to export.

    Each node needs to have a unique name.

  2. While in the Modeling menu set, use the main menu to click the Cache dropdown, then select Alembic Cache > Export Selection to Alembic.


  3. In the Export Selection window, under the General Options category, set the Cache time range to Current Frame.


  4. Under the Attributes category, type the name of the Attribute you want to the list and click the Add button. Add the following schema attributes:


    • groom_group_id

    • groom_guide

  5. Enter a file name in the File name textbox, and set the Files of type to Alembic.


  6. Click the Export Selection button.

Applying Textures to Hair UVs

The following steps and included script can help you set up your own XGen hair that can be exported to Unreal Engine, and have an applied texture represented on individual hair strands.

  1. In Maya use the Modeling menu to select Generate > Create Interactive Groom Splines.


  2. You can create guides and brush the hair as you like for your project. When ready, export the curves as an Alembic Cache by selecting Generate > Cache > Create New Cache.


  3. Remove your XGen hair by hiding or deleting it. Then, reimport your exported hair curves with your source mesh in the Maya scene.


  4. Depending on your scene, you will have thousands of spline curves parented under a top curve, SplineGrp0 in this example. Edit the following Python script and replace the following values with the one from your project:

    • export_directory

    • hair_file

    • curve_top_group

    • uv_mesh

    You can download the script below here .

    from maya import cmds
    from maya import OpenMaya
    import os
    def create_root_uv_attribute(curves_group, mesh_node, uv_set='map1'):
        Create "groom_root_uv" attribute on group of curves.
        # check curves group
        if not cmds.objExists(curves_group):
            raise RuntimeError('Group not found: "{}"'.format(curves_group))
        # get curves in group
        curve_shapes = cmds.listRelatives(curves_group, shapes=True, noIntermediate=True)
        curve_shapes = cmds.ls(curve_shapes, type='nurbsCurve')
        if not curve_shapes:
            raise RuntimeError('Invalid curves group. No nurbs-curves found in group.')
            print "found curves"
            print curve_shapes
        # get curve roots
        points = list()
        for curve_shape in curve_shapes:
            point = cmds.pointPosition('{}.cv[0]'.format(curve_shape), world=True)
        # get uvs
        values = list()
        uvs = find_closest_uv_point(points, mesh_node, uv_set=uv_set)
        for u, v in uvs:
            values.append([u, v, 0])
            #print (str(u) + " , " + str(v)  )
        # create attribute
        name = 'groom_root_uv'
        cmds.addAttr(curves_group, ln=name, dt='vectorArray')
        cmds.addAttr(curves_group, ln='{}_AbcGeomScope'.format(name), dt='string')
        cmds.addAttr(curves_group, ln='{}_AbcType'.format(name), dt='string')
        cmds.setAttr('{}.{}'.format(curves_group, name), len(values), *values, type='vectorArray')
        cmds.setAttr('{}.{}_AbcGeomScope'.format(curves_group, name), 'uni', type='string')
        cmds.setAttr('{}.{}_AbcType'.format(curves_group, name), 'vector2', type='string')
        return uvs
    def find_closest_uv_point(points, mesh_node, uv_set='map1'):
        Find mesh UV-coordinates at given points.
        # check mesh
        if not cmds.objExists(mesh_node):
            raise RuntimeError('Node not found: "{}"'.format(mesh_node))
        # check uv_set
        uv_sets = cmds.polyUVSet(mesh_node, q=True, allUVSets=True)
        if uv_set not in uv_sets:
            raise RuntimeError('Invalid uv_set provided: "{}"'.format(uv_set))
        # get mesh as dag-path
        selection_list = OpenMaya.MSelectionList()
        mesh_dagpath = OpenMaya.MDagPath()
        selection_list.getDagPath(0, mesh_dagpath)
        # get mesh function set
        fn_mesh = OpenMaya.MFnMesh(mesh_dagpath)
        uvs = list()
        for i in range(len(points)):
            script_util = OpenMaya.MScriptUtil()
            script_util.createFromDouble(0.0, 0.0)
            uv_point = script_util.asFloat2Ptr()
            point = OpenMaya.MPoint(*points[i])
            fn_mesh.getUVAtPoint(point, uv_point, OpenMaya.MSpace.kWorld, uv_set)
            u = OpenMaya.MScriptUtil.getFloat2ArrayItem(uv_point, 0, 0)
            v = OpenMaya.MScriptUtil.getFloat2ArrayItem(uv_point, 0, 1)
            uvs.append((u, v))
        return uvs
    def abc_export(filepath, node=None, start_frame=1, end_frame=1, data_format='otawa', uv_write=True):
        job_command = '-frameRange {} {} '.format(start_frame, end_frame)
        job_command += '-dataFormat {} '.format(data_format)
        job_command += '-attr groom_root_uv '
        if uv_write:
            job_command += '-uvWrite '
        job_command += '-root {} '.format(node)   
        job_command += '-file {} '.format(filepath) 
        cmds.AbcExport(verbose=True, j=job_command)
    def main():
        export_directory = 'D:/Dev/Ref'
        hair_file = os.path.join(export_directory, 'hair_export.abc')
        curve_top_group= 'description1|SplineGrp0'
        create_root_uv_attribute( curve_top_group , uv_mesh)
        abc_export(hair_file, curve_top_group)
  5. In Maya, run the script with the changed values to generate a new Alembic ('.abc') file which can be imported into Unreal Engine.

  6. In Unreal Engine, create a new Material using the Hair Shading Model. In the material graph, add a Hair Attributes expression and plug the Root UV into the UV input of the Texture Sample.


    The groom_root_uv attribute specifies per hair the underlying mesh UV which it is attached to. This attribute is optional, and if not specified, a root UV is automatically generated in the engine using a spherical mapping.

  7. Drag your imported hair Alembic file into the level from the Content Browser, and assign your hair material to it. You should end up with something like this:

    Make sure that your hair Alembic file in the level has a width greater than 0.


Select Skin
Help shape the future of Unreal Engine documentation! Tell us how we're doing so we can serve you better.
Take our survey

Welcome to the new Unreal Engine 4 Documentation site!

We're working on lots of new features including a feedback system so you can tell us how we are doing. It's not quite ready for use in the wild yet, so head over to the Documentation Feedback forum to tell us about this page or call out any issues you are encountering in the meantime.

We'll be sure to let you know when the new system is up and running.

Post Feedback