HeroEngine's Material Instancing System allows dynamic runtime modifications to the visual appearance of a model by changing the material of a specific instance to use a new texture, shader or material properties (bump, spec, etc). Material instancing is a huge win in games allowing the reuse of a model by making simple switches of the texture for variety. For example, an artist might have created the model of a red brick wall; using material instancing the polygons of the model can be reused but the texture can be switched to that of a stone wall.
Not only does material instancing support dynamic runtime modifications, but it can also be used as a normal part of the area editing process creating a persistent change. The system provides support to changing the features associated with one or more materials associated with an object and updating their properties. There are multiple options on the UI which can be selected for editing and viewed at runtime before saving to the server.
The system also makes it easy to create assets, change their material properties, save them to the asset library and reuse them in any desired area. Without material instancing, if the project required multiple stone walls an artist would have to create individual models for each with their unique texture mapping and use them at the appropriate places. Whereas material instancing saves the efforts of recreating similar materials and allows editing individual properties for the materials. For example, there can be a material named “Material_1” which has a specular map. Selecting the same material, we can update it to undo the specular map such that there will be a new material, say, “Material_2” on the server which will have all the properties similar to that of “Material_1” but won’t have the specular map.
This allows editing the appearance of a model at runtime and making multiple materials which can be used for different objects in an area.
- a template from which things in the game world are made. An asset instance is an object with a unique ID number, created from an asset. Areas are mainly populated with asset instances instantiated from the Asset Library. Assets own meshes
- a unique instantiation of an asset with its own set of mutable properties that may be modified through the Properties panel or script interfaces. Complex properties such as material instances use specialized property editors and serialize their data using a specialized Parameter syntax
- a single set of geometry, and a reference to the material which it uses. There may be multiple meshes in a model -- for example, a barrel of apples may consist of a mesh for the wooden barrel and a mesh for the pile of apples.
- a set of textures with defined roles (such as diffuse, normal/specular and macro) and associated properties. A material instance a unique combination of material properties which can be applied to one or more asset meshes. Material Instance Properties can be customized using the Material Instance Properties dialogue.
Use Material Instance Property Editor to Specify a Different Texture
- Log into any test area where changes won't be persisted (any play instance).
- Using the Asset Library, place two utility cubes in the area.
- Open the properties panel if it's not already visible.
- Select one of the cubes and navigate to the _MaterialOverrides property in the Properties Panel.
- Click on the empty space where the value for "_MaterialOverrides" would otherwise be. A "..." button will appear to the right. Click on that.
- The Material Instance Properties dialogue will appear. Click on the button with the diffuse map file name under "Diffuse/Alpha Map" and select any .dds file you like from the repository. The change you make will be immediately reflected in your viewport and to any other clients in your area instance.
- Press the close button and perform steps 4 - 6 on the other cube you placed in the area. Only, for the second cube, choose a different .dds file from the first. You'll notice that the two asset instances now have separate appearances due to their different material overrides.
Additional information about the dialogue and other customization options can be found on the Material Instance Properties wiki page.
Creating a Prefab Using Material Instances
Script Interface (HSL External Functions)
external function EnumerateMaterialInstanceProperties() as List of String
This function will return the following list of strings which can be use as the "property_name" parameter in the next two functions.
external function GetMaterialInstanceValue( model as NodeRef of Class HBNode, material_name as String, property_name as String ) as String
Retrieves the value of the property corresponding to the given material instance name and the asset instance (model) to which it has been applied.
external function SetMaterialInstanceValue( model as NodeRef of Class HBNode, material_name as String, property_name as String, new_value as String )
Applies the value to the property corresponding to the given material instance name and the asset instance (model) to which it has been applied.
external function ListNodeMaterials(model as NodeRef of Class HBNode) as List of String
Returns a list of serialized override strings which represents the collection of material overrides applied to a given asset instance. This is the same string which will be located in the Properties Panel under the _MaterialOverries field for any selected asset instance.
Using the Script Interface to Switch to a Different Texture
This is a simple tutorial on how to use the script interface for modifying the material instance properties of an asset.
- Use the DOM Editor to create a class. Give the class a unique name so it does not conflict with anything which already exists in the DOM. From this step onward, replace the text "YourClassName" with the name you choose for your class.
- Open the script for your newly created class and paste the following HSL code into it. Remember to change "YourClassName" with the name of the class you created in step 1.
//This function creates the propbucket and adds the asset spec to that propbucket. //Adding the spec to the prop bucket is an asynchronous procedure which requires a separate callback method. //The nateNoderef noderef is an instance of this class which will respond to the callback AssetSpecReady() function CreateAssetPropBucket() CreatePropBucket( "MATERIALINSTANCESTEST" ) cubeFQN as String = "\engine\cleangame\resources\common\utility_box_white01.gr2" yourClassNameNodeRef as NodeRef of Class YourClassName = CreateNodeFromClass("YourClassName ") spec as NodeRef of Class HBSpec = AddAssetSpecToPropBucketCB( "MATERIALINSTANCESTEST", cubeFQN, yourClassNameNodeRef ) . //Asynchronous callback resulting from calling the external function AddAssetSpecToPropBucketCB() method AssetSpecReady(spec as NodeRef of Class HBSpec, loadFailed as Boolean) if loadFailed return . //Instance the HBNodes from the prop bucket cubeA as NodeRef of Class HBNode = CreateInstanceFromPropBucket( "MATERIALINSTANCESTEST", spec ) cubeB as NodeRef of Class HBNode = CreateInstanceFromPropBucket( "MATERIALINSTANCESTEST", spec ) defaultcube as NodeRef of Class HBNode = CreateInstanceFromPropBucket( "MATERIALINSTANCESTEST", spec ) //Set the HBNode positions to the left and right of the player character //A default cube will be placed in front of the character for reference. It will not be modified. playerPosition as Vector3 GetNodePosition(SYSTEM.INFO.PLAYERCHARACTER, playerPosition) cubeAPosition as Vector3 = playerPosition + (0.3,0,0) cubeBPosition as Vector3 = playerPosition - (0.3,0,0) defaultcubePosition as Vector3 = playerPosition + (0,0,-0.3) SetNodePosition( cubeA, cubeAPosition) SetNodePosition( cubeB, cubeBPosition) SetNodePosition( defaultcube, defaultcubePosition) //make the HBNodes renderable ActivateInstance( cubeA, "") ActivateInstance( cubeB, "") ActivateInstance( defaultcube, "") //print all the material properties available materialParameterNames as List of String = EnumerateMaterialInstanceProperties() loop i from 1 to materialParameterNames.length println("Property[" + i + "] = $Q" + materialParameterNames[i] + "$Q") . //list the materials names which currently exist on the HBNode materialsOnNode as List of String = ListNodeMaterials(defaultcube) println(materialsOnNode.length + " material(s) found on HBNode") loop i from 1 to materialsOnNode.length println("[" + i + "] " + materialsOnNode[i]) . //GetMaterialInstanceValue() gets a specific material instance property //At this point, the diffuse maps should be set to their default values diffusePathA as String = GetMaterialInstanceValue(cubeA, materialsOnNode, materialParameterNames) diffusePathB as String = GetMaterialInstanceValue(cubeB, materialsOnNode, materialParameterNames) //print the default diffuse map paths println("cubeA(" + cubeA + ") default diffuse map path = " + diffusePathA) println("cubeB(" + cubeB + ") default diffuse map path = " + diffusePathB) //SetMaterialInstanceValue() sets a specific material instance property //These two operations set the diffuse maps of the left and right cubes to different textures from their defaults SetMaterialInstanceValue(cubeA, materialsOnNode, materialParameterNames, "\Engine\CleanGame\Resources\Common\Textures\wagon1.dds") SetMaterialInstanceValue(cubeB, materialsOnNode, materialParameterNames, "\Engine\CleanGame\Resources\Common\Textures\checkered01.dds") //Get the new diffuse map paths from the HBNodes diffusePathA = GetMaterialInstanceValue(cubeA, materialsOnNode, materialParameterNames) diffusePathB = GetMaterialInstanceValue(cubeB, materialsOnNode, materialParameterNames) //print the new diffuse map paths println("cubeA(" + cubeA + ") new diffuse map path = " + diffusePathA) println("cubeB(" + cubeB + ") new diffuse map path = " + diffusePathB) .
Changes made through the Material Instance Properties dialogue are represented in a serialized override string. Values which differ from the base material will appear in the string within the _MaterialOverrides property field in the Properties Panel.
The serialization format is as follows.
<serialization format version number> % <instance name> # <hash string for this material instance> [ <diffuse map path>,<diffuse map alpha type>,<normal map path>,<normal map contains specular channel>,<macro map path>,<macro map UV multiplier>,<shader map path>,<shader translucency>,<shader reflectivity>,<texture address mode U>,<texture address mode V>,<polygon attributes>,<animated UVs U1>,<animated UVs V1>,<animated UVs U2>,<animated UVs V2>]
Here is an example of a material override string after changing the diffuse map path. Note that some values are empty and others contain default enumerations which are set by the system.