Houdini Engine 1.9
 All Classes Files Functions Variables Typedefs Enumerations Enumerator Macros Pages
Materials

Materials Introduction

Materials in Houdini Engine are mappeded one-to-one to Houdini SHOP nodes. You track them using HAPI_MaterialId's which are unique and re-used at the asset level but cannot be interchanged globally between different assets. This is why all material APIs require the HAPI_AssetId.

The materials API is spilt into three stages and what you do in one stage does not affect what you can do in the next stage. The first stage is material assignment extraction where you get the materials assigned to each face of your mesh. Next, we have rendering to image where you render a material or just a single texture of a material to an internally cached "image". Finally, you extract the image out of HAPI and into either a memory buffer or a file.

Additionally, it is important to keep track of material updates to avoid unnecessary rendering and image extraction. Two important changed flags exist for this purpose.

Get Material Assignments

Material assignments can be retrieved on a Part using the HAPI_GetMaterialIdsOnFaces() call. It expects an array of HAPI_MaterialId's the size of at most HAPI_PartInfo::faceCount.

If the material is assigned at the asset level all faces will have the same material id assigned and you have a convenient are_all_the_same output parameter on HAPI_GetMaterialIdsOnFaces() that saves you from checking each face to find out they are all the same. Here's a quick sample that queries a part's material assignment where the part has its material assigned at the object level:

// Load the library from file.
HAPI_AssetLibraryId library_id = -1;
HAPI_Result result =
"HAPI_Test_Materials_Simple.otl", false, &library_id );
assert( result == HAPI_RESULT_SUCCESS );
assert( library_id >= 0 );
// Instantiate the asset.
HAPI_AssetId asset_id = -1;
"Object/HAPI_Test_Materials_Simple", true, &asset_id );
assert( result == HAPI_RESULT_SUCCESS );
// Get and check the HAPI_PartInfo. In this case, we know we're
// specifically looking for the part 0 on object 0 and geo 0.
HAPI_PartInfo part_info;
result = HAPI_GetPartInfo( asset_id, 0, 0, 0, &part_info );
assert( result == HAPI_RESULT_SUCCESS );
assert( part_info.faceCount > 0 );
// Now call HAPI_GetMaterialIdsOnFaces() with the number of faces as the
// number of material ids since it is guaranteed to be a 1:1 relation.
bool are_all_the_same = false;
HAPI_MaterialId* material_ids = new HAPI_MaterialId[ part_info.faceCount ];
asset_id, 0, 0, 0,
&are_all_the_same, material_ids,
0, part_info.faceCount );
assert( result == HAPI_RESULT_SUCCESS );
// Note that for this asset, all faces have the same material assignment
// so we can make some optimization/assumptions based on the returned
// are_all_the_same variable without having to go through the entire list
// of material ids.
assert( are_all_the_same == true );
// Check to make sure indeed all material ids are identical.
for ( int i = 0; i < part_info.faceCount; ++i )
assert( material_ids[ i ] == material_ids[ 0 ] );
delete [] material_ids;

Material assigments can also be assigned via the SHOP path primitive attribute in which case you might get different material ids on each face. Here is an example of an asset that has different material assignments on each face:

// Load the library from file.
HAPI_AssetLibraryId library_id = -1;
HAPI_Result result =
"HAPI_Test_Materials_Multi.otl", false, &library_id );
assert( result == HAPI_RESULT_SUCCESS );
assert( library_id >= 0 );
// Instantiate the asset.
HAPI_AssetId asset_id = -1;
"Object/HAPI_Test_Materials_Multi", true, &asset_id );
assert( result == HAPI_RESULT_SUCCESS );
// Get "Multi_Partial" object info.
// See docs on Objects/Geos/Parts for how to implement HAPIgetObjectInfo().
HAPI_ObjectInfo object_info;
assert( HAPIgetObjectInfo( asset_id, "Multi_Partial", &object_info ) );
// Get and check the HAPI_PartInfo. In this case, we know we're
// specifically looking for the part 0 on object "Multi_Partial" and geo 0.
HAPI_PartInfo part_info;
result = HAPI_GetPartInfo( asset_id, object_info.id, 0, 0, &part_info );
assert( result == HAPI_RESULT_SUCCESS );
assert( part_info.faceCount > 0 );
// Now call HAPI_GetMaterialIdsOnFaces() with the number of faces as the
// number of material ids since it is guaranteed to be a 1:1 relation.
bool are_all_the_same = false;
HAPI_MaterialId* material_ids = new HAPI_MaterialId[ part_info.faceCount ];
asset_id, object_info.id, 0, 0,
&are_all_the_same, material_ids,
0, part_info.faceCount );
assert( result == HAPI_RESULT_SUCCESS );
// For this asset, not all faces will have the same material assignment.
// Therefore, this returned variable will be false.
assert( are_all_the_same == false );
// Check material memberships.
for ( int face_idx = 0; face_idx < part_info.faceCount; ++face_idx )
{
// Some faces have no material assigment at all. They will have
// a material id of -1. Skip them.
if ( ( 50 <= face_idx && face_idx <= 53 ) || ( face_idx >= 59 ) )
{
assert( material_ids[ face_idx ] == -1 );
continue;
}
// First, get the HAPI_MaterialInfo of the current material.
HAPI_MaterialInfo material_info;
asset_id, material_ids[ face_idx ], &material_info );
assert( result == HAPI_RESULT_SUCCESS );
// To get access to the material parameters, get the HAPI_NodeInfo
// of the material's SHOP node via the ::HAPI_MaterialInfo::nodeId.
HAPI_NodeInfo node_info;
result = HAPI_GetNodeInfo( material_info.nodeId, &node_info );
assert( result == HAPI_RESULT_SUCCESS );
// We now check certain memberships for specific materials.
// See docs on HAPI_GetString() for implementation of
// HAPItestGetString().
if ( face_idx <= 4 )
assert( HAPItestGetString( node_info.nameSH ) == "Multi_Partial1");
else if ( 5 <= face_idx && face_idx <= 8 )
assert( HAPItestGetString( node_info.nameSH ) == "Multi_Partial2");
else if ( 9 <= face_idx && face_idx <= 13 )
assert( HAPItestGetString( node_info.nameSH ) == "Multi_Partial1");
else if ( 14 <= face_idx && face_idx <= 17 )
assert( HAPItestGetString( node_info.nameSH ) == "Multi_Partial2");
else if ( 18 <= face_idx && face_idx <= 22 )
assert( HAPItestGetString( node_info.nameSH ) == "Multi_Partial1");
else if ( 23 <= face_idx && face_idx <= 26 )
assert( HAPItestGetString( node_info.nameSH ) == "Multi_Partial2");
else if ( 27 <= face_idx && face_idx <= 31 )
assert( HAPItestGetString( node_info.nameSH ) == "Multi_Partial1");
else if ( 32 <= face_idx && face_idx <= 35 )
assert( HAPItestGetString( node_info.nameSH ) == "Multi_Partial2");
else if ( 36 <= face_idx && face_idx <= 40 )
assert( HAPItestGetString( node_info.nameSH ) == "Multi_Partial1");
else if ( 41 <= face_idx && face_idx <= 44 )
assert( HAPItestGetString( node_info.nameSH ) == "Multi_Partial2");
else if ( 45 <= face_idx && face_idx <= 49 )
assert( HAPItestGetString( node_info.nameSH ) == "Multi_Partial1");
else if ( 54 <= face_idx && face_idx <= 58 )
assert( HAPItestGetString( node_info.nameSH ) == "Multi_Partial1");
}
delete [] material_ids;

Finally, once you have your material ids, you call HAPI_GetMaterialInfo() to get the HAPI_MaterialInfo, or more importantly, the HAPI_MaterialInfo::nodeId. The node id lets you access the parameters on the SHOP node that this material is tied to and lets you manipulate the materials properties and extract individual textures. Here's an example of extracting the HAPI_MaterialInfo:

// Load the library from file.
HAPI_AssetLibraryId library_id = -1;
HAPI_Result result =
"HAPI_Test_Materials_Simple.otl", false, &library_id );
assert( result == HAPI_RESULT_SUCCESS );
assert( library_id >= 0 );
// Instantiate the asset.
HAPI_AssetId asset_id = -1;
"Object/HAPI_Test_Materials_Simple", true, &asset_id );
assert( result == HAPI_RESULT_SUCCESS );
// Get the material id on the first face on object 0, geo 0, and part 0.
HAPI_MaterialId material_id;
asset_id, 0, 0, 0, NULL, &material_id, 0, 1 );
assert( result == HAPI_RESULT_SUCCESS );
// From this material id you can get the HAPI_MaterialInfo using
// ::HAPI_GetMaterialInfo().
HAPI_MaterialInfo material_info;
result = HAPI_GetMaterialInfo( asset_id, material_id, &material_info );
assert( result == HAPI_RESULT_SUCCESS );
// This is the first time we request information on this material so
// it will have just been created in HAPI. Therefore, the
// ::HAPI_MaterialInfo::hasChanged flag will be true.
assert( material_info.id == material_id );
assert( material_info.assetId == asset_id );
assert( material_info.nodeId >= 0 );
assert( material_info.exists == true );
assert( material_info.hasChanged == true );
// To get access to the material parameters, get the HAPI_NodeInfo
// of the material's SHOP node via the ::HAPI_MaterialInfo::nodeId.
HAPI_NodeInfo node_info;
result = HAPI_GetNodeInfo( material_info.nodeId, &node_info );
assert( result == HAPI_RESULT_SUCCESS );
// Here we just check the name of this material's SHOP node.
// See docs on HAPI_GetString() for implementation of
// HAPItestGetString().
assert( HAPItestGetString( node_info.nameSH ) == "JPEG" );

Rendering to Image

There are two options for rendering an image. You must render to image using one of the options before the rest of the material APIs work. The images rendered will be cached per material so you can render a whole bunch of materials and extract the images afterwards. This also means that an image render will always overwrite the previous image render on the same material. Both options require a material the HAPI_MaterialInfo::id. You can get a HAPI_MaterialInfo on a Part, using HAPI_GetMaterialOnPart(), or per Group using HAPI_GetMaterialOnGroup(). You'll want to use HAPI_GetMaterialOnPart() only if you're splitting geos into parts by group using HAPI_CookOptions::splitGeosByGroup set to true. If you're not splitting geos into parts by group then you might have multiple materials on a single part (one material per group) so you'll want to use HAPI_GetMaterialOnGroup(). But nothing will explode if you intermix use of these two functions.

Rendering Texture Map to Image

The first option is to render a single texture map to image.

Using the HAPI_MaterialInfo::nodeId you can use the regular parameter access functions, as described in the Parameters section, to get the parameter id that contains the texture map path you’re looking for. Here’s what a typical material node parameters looks like:

HAPI_Materials_MaterialParameters.png

What you’re looking for are the different texture map parameters, like Diffuse, Normal, Bump, etc. For example, most materials will store the diffuse map in the Base Color Map (baseColorMap) parameter:

HAPI_Materials_TextureMaps.png

Now you can finally render the texture map to the internally cached image, using HAPI_RenderTextureToImage().

Rendering Material to Image

The second option is to render the entire material, with all the VEX-based Houdini shader logic being applied, to an image. Before invoking this render you will need to tweak the global Mantra Render node parameters. This is a node created by Houdini Engine at startup that is used for all such material renderings. It is a node shared by all assets and all materials.

A note on some restrictions that you have if you choose to render a material using Mantra:

  1. The object rendered has to be a polygon mesh. No NURBS surfaces, for example. Even though Houdini Engine exports polygons anyway, the rendering is happening internally where the geometry is in whatever form the asset creator left it in.
  2. The "uv" attribute must exist and it must be a Vertex attribute.
  3. All UVs must be within the positive 0-1 UV space.

Like all other node parameters, you will need the global Mantra Render node's id. You can get this using HAPI_GetGlobalNodes(). This call returns a HAPI_GlobalNodes. The HAPI_NodeId you're looking for is HAPI_GlobalNodes::mantraRenderer. Now follow the steps in Setting Values to modify the following relevant parameters on the Mantra node:

HAPI_Materials_MantraParameters.png

When a material is rendered it can generate one or more image planes or layers. The texture maps coming in will have their own image planes but the material may use them or it may not. So the image planes available to you if you render the entire material depend on what the material’s shader decided to generate. It is not possible currently to query available image planes at the material level so you will need to consult with the asset creator to get a full list.

Once you have a list of available material image planes you will need to use the MultiParms section to add a multiparm for each image plane to the "Extra Image Planes" (vm_numaux) multiparm parameter:

HAPI_Materials_ExtraImagePlanes.png

The color or diffuse plane, called "C", is implied so it is not listed under "Extra Image Planes" and usually the normals plane, called "N", will be exported by default (as seen in the screenshot above) but any additional planes have to be added manually.

Don’t worry about adding image planes that the material doesn’t support. Unsupported image planes will just be rendered black but nothing should break.

Feel free to change other parameters on the global Mantra Render node too but you should now be ready to actually render this material. Remember that you need to do all this Mantra node setup before every time you render a material to image because all other materials share this same global node so another material could have changed the settings.

To render the material to image use HAPI_RenderMaterialToImage(). For the shader_type argument just pass in HAPI_SHADER_MANTRA. The HAPI_SHADER_OPENGL option is currently only there as a placeholder and is not supported.

Extracting Images

After you've rendered an image using the options above in the Rendering to Image section you're ready for the image extraction step, either to a file or in-memory. But before actually extracting the image you might want to manipulate the rendered image to save you from doing it on the client side.

Image Manipulation

Currently, the only property of an image you can change is the resolution. If you choose to use in-memory image extraction and you use our built-in HAPI-specific RAW file (and only the RAW file format) format then you can also change the data format (ie. int8, int16, float16, etc.), whether the image is interleaved (RGBRGB vs. RRGGBB), and what packing to use (number and order of channels).

All image properties are accessed / modified using HAPI_GetImageInfo() / HAPI_SetImageInfo().

You should call HAPI_GetImageInfo() first to get the original image properties as a HAPI_ImageInfo struct and make only the changes you wish before calling HAPI_SetImageInfo() to apply. The HAPI_ImageInfo::imageFileFormatNameSH parameter is readonly because the file format is specified right at extraction time. See the section below on Image File Formats for more on this.

Image File Formats

Houdini supports several image file formats out of the box as one would expect. However, you can also implement custom file formats for Houdini using dynamically loaded libraries. See the HDK documentation for more information, specifically: http://www.sidefx.com/docs/hdk13.0/hdk_intro_creatingplugins.html and look at the "Image Formats" category example plugin. As a general rule, all plugins you write or use in Houdini will work with Houdini Engine, including custom file formats.

For efficiency reasons, it is recommended you check the original file format of a rendered image using HAPI_GetImageInfo() and only request a different file format when extracting if your client does not support the original format.

You can query the list of Houdini-supported file formats by first getting the count with HAPI_GetSupportedImageFileFormatCount() and then filling your list of HAPI_ImageFileFormat's using HAPI_GetSupportedImageFileFormats().

The HAPI_ImageFileFormat::nameSH of the format will be its full name, the HAPI_ImageFileFormat::descriptionSH usually contains copyright or licensing information, while the HAPI_ImageFileFormat::defaultExtensionSH is the extension Houdini will use when creating a new file of this format. Houdini may recognize multiple extensions per format (ie. .jpg and .jpeg) so the HAPI_ImageFileFormat::defaultExtensionSH is only for new files.

When specifying a file format in the extraction step you must use the format name as contained in the HAPI_ImageFileFormat::nameSH field. Here are some standard Houdini-supported format names, as defined in HAPI_Common.h:

The HAPI_RAW_FORMAT_NAME deserves special attention. This is a format only available through Houdini Engine. It has no header or meta data of any kind. It only works for in-memory image extraction so you cannot create a texture file using this format. The layout is exactly what the name implies: raw, uncompressed, color information. See the Image Manipulation section above for details on how to control the color information memory layout of the HAPI_RAW format.

A final note on file formats is whether they support deep image planes or not. Most formats will not support deep image planes which means that they will only be able to store a single image plane per image. The only format natively supported by Houdini that can handle multiple image planes per image is Houdini’s .pic format.

Image Planes

An image can have multiple image planes or layers. The standard two are the diffuse plane, named "C", and the alpha plane, named "A". Of course, an image can have any number of standard and non-standard planes like normal, bump, and tangent planes. During the extraction step you will need to specify the image planes you want to include in the extracted image by name.

To get the list of available image planes for an image, first get the plane count using HAPI_GetImagePlaneCount(). Then, get the plane names (array of string handles) using HAPI_GetImagePlanes().

Image Extraction

We are finally ready to extract our image. You extract your image to a file or completely in-memory in binary form.

To extract your image to a file use HAPI_ExtractImageToFile(). To extract your image in-memory use HAPI_ExtractImageToMemory(). You will get back a buffer_size which you need to use to allocate a byte array of this size and pass it to HAPI_GetImageMemoryBuffer() which will fill it with the image data.

You can call HAPI_ExtractImageToFile() and HAPI_ExtractImageToMemory() as many times as you wish for an image. You can also control which image planes you want to extract per image. For example, you can extract the color plane first, with image_planes set to "C", and then extract the normals plane as a separate image next, with image_planes set to "N", as shown below:

HAPI_Materials_NormalMap.png

If your image format supports multiple image planes per image, like Houdini's .pic format, you can call HAPI_ExtractImageToFile() or HAPI_ExtractImageToMemory() with image_planes set to "C N" to get both planes at once.

Material Updates

All of this work in getting material assignments, rendering, and then extracting images, can cause performance problems if done all the time. This is why it is important to use the various changed flags to determine when a material needs to be refreshed.

There are two such changed flags that affect materials:

  1. One is HAPI_MaterialInfo::hasChanged flag, which will be set to true if something (anything) on that material or one of its dependencies has changed. For example, if the diffuse color is controlled via a top-level asset parameter and the user changed this color. If this flag is true, you just need to render/extract the texture/material you were extracting before. You do not need to re-fetch material assignments or query anything on the geo or part. Only the material has changed.
  2. The other flag is the HAPI_GeoInfo::hasGeoChanged which will tell you that, among other mesh changes, the material assignments could have changed. You need to call HAPI_GetMaterialIdsOnFaces() again. That said, apart from new materials that may have been assigned, any previously tracked/extract materials that have just changed their assignments do not need to be re-rendered and re-extracted. In this case, only material assignments have changed and not the materials themselves.

Full Source Sample

See Materials Sample for a complete program that renders and extracts textures.