/// <summary> /// Query object nodes in the Asset. An object node represents a Houdini transform node. /// Each object might have any number of SOP geometry containers and a transform. /// <param name="houdiniAsset">The HEU_HoudiniAsset of the loaded asset</param> /// </summary> public static void QueryObjects(HEU_HoudiniAsset houdiniAsset) { // Get access to the Houdini Engine session used by this asset. // This gives access to call Houdini Engine APIs directly. HEU_SessionBase session = houdiniAsset.GetAssetSession(true); if (session == null || !session.IsSessionValid()) { Debug.LogWarningFormat("Invalid Houdini Engine session! Try restarting session."); return; } HAPI_ObjectInfo[] objectInfos = null; HAPI_Transform[] objectTransforms = null; HAPI_NodeInfo assetNodeInfo = houdiniAsset.NodeInfo; // Fill in object infos and transforms based on node type and number of child objects. // This the hiearchy of the HDA when loaded in Houdini Engine. It can contain subnets with // multiple objects containing multiple geometry, or a single object containting any number of geometry. // This automatically handles object-level HDAs and geometry (SOP) HDAs. if (!HEU_HAPIUtility.GetObjectInfos(session, houdiniAsset.AssetID, ref assetNodeInfo, out objectInfos, out objectTransforms)) { return; } // For each object, get the display and editable geometries contained inside. for (int i = 0; i < objectInfos.Length; ++i) { // Get display SOP geo info HAPI_GeoInfo displayGeoInfo = new HAPI_GeoInfo(); if (!session.GetDisplayGeoInfo(objectInfos[i].nodeId, ref displayGeoInfo)) { return; } QueryGeoParts(session, ref displayGeoInfo); // Optional: Get editable nodes, cook em, then create geo nodes for them HAPI_NodeId[] editableNodes = null; HEU_SessionManager.GetComposedChildNodeList(session, objectInfos[i].nodeId, (int)HAPI_NodeType.HAPI_NODETYPE_SOP, (int)HAPI_NodeFlags.HAPI_NODEFLAGS_EDITABLE, true, out editableNodes); if (editableNodes != null) { foreach (HAPI_NodeId editNodeID in editableNodes) { if (editNodeID != displayGeoInfo.nodeId) { session.CookNode(editNodeID, HEU_PluginSettings.CookTemplatedGeos); HAPI_GeoInfo editGeoInfo = new HAPI_GeoInfo(); if (session.GetGeoInfo(editNodeID, ref editGeoInfo)) { QueryGeoParts(session, ref editGeoInfo); } } } } } }
protected override bool buildCreateObjects(bool reload_asset, ref HoudiniProgressBar progress_bar) { try { prCurve.syncPointsWithParm(); HAPI_GeoInfo geo_info = HoudiniHost.getDisplayGeoInfo(prCurve.prControl.prAssetId); HAPI_NodeInfo geo_node_info = HoudiniHost.getNodeInfo(geo_info.nodeId); prCurve.createObject(geo_node_info.parentId, geo_info.nodeId); HoudiniHost.repaint(); } catch (HoudiniError) { // Per-object errors are not re-thrown so that the rest of the asset has a chance to load. //Debug.LogWarning( error.ToString() ); } return(false); }
public void getParameterValues() { if (prControl == null) { return; } if (prControl.prAsset == null) { return; } if (prControl.prAssetId < 0) { return; } if (myPostSerialization) { myValuesEqualToHoudini = areValuesEqualToHoudini(); } else { myValuesEqualToHoudini = true; } // Create undo info if it hasn't been created already if (myParmsUndoInfo == null) { myParmsUndoInfo = ScriptableObject.CreateInstance <HoudiniParmsUndoInfo>(); } // Get the node info again HAPI_NodeInfo node_info = HoudiniHost.getNodeInfo(prControl.prNodeId); prParmCount = node_info.parmCount; prParmIntValueCount = node_info.parmIntValueCount; prParmFloatValueCount = node_info.parmFloatValueCount; prParmStringValueCount = node_info.parmStringValueCount; prParmChoiceCount = node_info.parmChoiceCount; // Get all parameters. prParms = new HAPI_ParmInfo[prParmCount]; HoudiniAssetUtility.getArray1Id(prControl.prNodeId, HoudiniHost.getParameters, prParms, prParmCount); // Get parameter int values. prParmIntValues = new int[prParmIntValueCount]; HoudiniAssetUtility.getArray1Id( prControl.prNodeId, HoudiniHost.getParmIntValues, prParmIntValues, prParmIntValueCount); myParmsUndoInfo.parmIntValues = new int[prParmIntValueCount]; Array.Copy(prParmIntValues, myParmsUndoInfo.parmIntValues, prParmIntValueCount); // Get parameter float values. prParmFloatValues = new float[prParmFloatValueCount]; HoudiniAssetUtility.getArray1Id( prControl.prNodeId, HoudiniHost.getParmFloatValues, prParmFloatValues, prParmFloatValueCount); myParmsUndoInfo.parmFloatValues = new float[prParmFloatValueCount]; Array.Copy(prParmFloatValues, myParmsUndoInfo.parmFloatValues, prParmFloatValueCount); // Get parameter string (handle) values. prParmStringValues = new int[prParmStringValueCount]; HoudiniAssetUtility.getArray1Id( prControl.prNodeId, HoudiniHost.getParmStringValues, prParmStringValues, prParmStringValueCount); // Get parameter choice lists. prParmChoiceLists = new HAPI_ParmChoiceInfo[prParmChoiceCount]; HoudiniAssetUtility.getArray1Id( prControl.prNodeId, HoudiniHost.getParmChoiceLists, prParmChoiceLists, prParmChoiceCount); // Build the map of parm id -> parm for (int i = 0; i < prParms.Length; ++i) { myParmMap[prParms[i].id] = prParms[i]; } cacheStringsFromHost(); // Go through parameters and set index map and multiparm map for undo info myParmsUndoInfo.parmNames.Clear(); myParmsUndoInfo.parmIndices.Clear(); foreach (HAPI_ParmInfo parm in prParms) { // Need to check the index values are greater or equal to 0 // for now because there is a bug where some parameters are // being set to have an integer parameter type, a size of // zero, and an index value of -1 if (parm.isInt() && parm.intValuesIndex >= 0) { myParmsUndoInfo.parmIndices.Add(parm.intValuesIndex); } else if (parm.isFloat() && parm.floatValuesIndex >= 0) { myParmsUndoInfo.parmIndices.Add(parm.floatValuesIndex); } else if (parm.isString() && parm.stringValuesIndex >= 0) { myParmsUndoInfo.parmIndices.Add(parm.stringValuesIndex); } else { continue; } myParmsUndoInfo.parmNames.Add(parm.name); } #if UNITY_EDITOR // Set which parameter values have been overridden (only needed for a prefab instance) if (prControl && prControl.isPrefabInstance() && gameObject.GetComponent <HoudiniAsset>() != null) { HoudiniAsset prefab_asset = prControl.prAsset.getParentPrefabAsset(); if (prefab_asset && prefab_asset.prParms != null && prefab_asset.prParms.prParms != null && !prefab_asset.isApplyingChangesToPrefab()) { // loop through parameter values and determine which ones have been // overridden (ie. changed from corresponding parameter value on prefab) for (int i = 0; i < prParms.Length; ++i) { myOverriddenParmsMap[prParms[i].id] = !isParmSameInPrefab(prParms[i].id, prefab_asset.prParms); } } // This tells Unity that parameter values have been overridden for this prefab instance PrefabUtility.RecordPrefabInstancePropertyModifications(this); } #endif // UNITY_EDITOR }
public bool areValuesEqualToHoudini() { if (prControl == null) { return(false); } if (prControl.prAsset == null) { return(false); } if (prControl.prAssetId < 0) { return(false); } // Get the node info again HAPI_NodeInfo node_info = HoudiniHost.getNodeInfo(prControl.prNodeId); if (prParmCount != node_info.parmCount) { return(false); } if (prParmIntValueCount != node_info.parmIntValueCount) { return(false); } if (prParmFloatValueCount != node_info.parmFloatValueCount) { return(false); } if (prParmStringValueCount != node_info.parmStringValueCount) { return(false); } if (prParmChoiceCount != node_info.parmChoiceCount) { return(false); } // Get parameter int values. int[] houdini_int_values = new int[prParmIntValueCount]; HoudiniAssetUtility.getArray1Id( prControl.prNodeId, HoudiniHost.getParmIntValues, houdini_int_values, prParmIntValueCount); if (prParmIntValues.Length != houdini_int_values.Length) { return(false); } for (int i = 0; i < prParmIntValueCount; ++i) { if (!prParmIntValues[i].Equals(houdini_int_values[i])) { return(false); } } // Get parameter float values. float[] houdini_float_values = new float[prParmFloatValueCount]; HoudiniAssetUtility.getArray1Id( prControl.prNodeId, HoudiniHost.getParmFloatValues, houdini_float_values, prParmFloatValueCount); if (prParmFloatValues.Length != houdini_float_values.Length) { return(false); } for (int i = 0; i < prParmFloatValueCount; ++i) { if (!prParmFloatValues[i].Equals(houdini_float_values[i])) { return(false); } } // Get parameter string (handle) values. int[] houdini_string_values = new int[prParmStringValueCount]; HoudiniAssetUtility.getArray1Id( prControl.prNodeId, HoudiniHost.getParmStringValues, houdini_string_values, prParmStringValueCount); if (prParmStringValues.Length != houdini_string_values.Length) { return(false); } for (int i = 0; i < prParmStringValueCount; ++i) { if (!HoudiniHost.getString(prParmStringValues[i]).Equals( HoudiniHost.getString(houdini_string_values[i]))) { return(false); } } return(true); }
public bool setNodeParameterIntoHost(int id) { HAPI_ParmInfo parm = findParm(id); var parm_input = myParmInputs[id]; if (!parm_input.inputObject) { return(false); } // Create new input node. GameObject input_object = parm_input.inputObject; HoudiniControl houdini_control = input_object.GetComponent <HoudiniControl>(); MeshFilter mesh_filter = input_object.GetComponent <MeshFilter>(); if (houdini_control && houdini_control.prAsset.gameObject.GetComponentInChildren <HoudiniGeoControl>()) { if (!houdini_control.prAsset.isAssetValid()) { houdini_control.prAsset.buildAll(); } HoudiniGeoControl geo_control = houdini_control.prAsset.gameObject.GetComponentInChildren <HoudiniGeoControl>(); parm_input.inputNodeId = geo_control.prNodeId; // Add ourselves to our input asset's downstream nodes so when it cooks we cook. houdini_control.prAsset.addDownstreamAsset(myControl.prAsset); } else if (houdini_control && input_object.GetComponent <HoudiniAssetCurve>()) { if (!houdini_control.prAsset.isAssetValid()) { houdini_control.prAsset.buildAll(); } parm_input.inputNodeId = houdini_control.prNodeId; // Add ourselves to our input asset's downstream nodes so when it cooks we cook. houdini_control.prAsset.addDownstreamAsset(myControl.prAsset); } else if (mesh_filter && mesh_filter.sharedMesh) { // We need to remove spaces in the input name string inputName = input_object.name.Replace(' ', '_'); parm_input.inputNodeId = HoudiniHost.createInputNode(inputName); Mesh mesh = mesh_filter.sharedMesh; HoudiniAssetUtility.setMesh( parm_input.inputNodeId, 0, parm_input.inputNodeId, ref mesh, null, null); // Set the asset transform from the source GameObject transform. HAPI_TransformEuler trans = HoudiniAssetUtility.getHapiTransform(input_object.transform.localToWorldMatrix); HAPI_NodeInfo input_node_info = HoudiniHost.getNodeInfo(parm_input.inputNodeId); HoudiniHost.setObjectTransform(input_node_info.parentId, ref trans); } else { return(false); } HAPI_NodeInfo node_info = HoudiniHost.getNodeInfo(parm_input.inputNodeId); parm_input.inputNodeUniqueId = node_info.uniqueHoudiniNodeId; // Assign node parm input. HoudiniHost.setParmNodeValue(prControl.prNodeId, parm.name, parm_input.inputNodeId); myParmInputs[id] = parm_input; return(true); }
public void getParameterValues() { if (prControl == null) { return; } if (prControl.prAsset == null) { return; } if (prControl.prAssetId < 0) { return; } if (myPostSerialization) { myValuesEqualToHoudini = areValuesEqualToHoudini(); } else { myValuesEqualToHoudini = true; } // Create undo info if it hasn't been created already if (myParmsUndoInfo == null) { myParmsUndoInfo = ScriptableObject.CreateInstance <HoudiniParmsUndoInfo>(); } // Get the node info again HAPI_NodeInfo node_info = HoudiniHost.getNodeInfo(prControl.prNodeId); prParmCount = node_info.parmCount; prParmIntValueCount = node_info.parmIntValueCount; prParmFloatValueCount = node_info.parmFloatValueCount; prParmStringValueCount = node_info.parmStringValueCount; prParmChoiceCount = node_info.parmChoiceCount; // Get all parameters. prParms = new HAPI_ParmInfo[prParmCount]; myParmInfoStrings = new HAPI_ParmInfoStrings[prParmCount]; if (myParmInputs == null) { myParmInputs = new HAPI_ParmInput[prParmCount]; } HoudiniAssetUtility.getArray1Id(prControl.prNodeId, HoudiniHost.getParameters, prParms, prParmCount); // Get parameter int values. prParmIntValues = new int[prParmIntValueCount]; HoudiniAssetUtility.getArray1Id( prControl.prNodeId, HoudiniHost.getParmIntValues, prParmIntValues, prParmIntValueCount); myParmsUndoInfo.parmIntValues = new int[prParmIntValueCount]; Array.Copy(prParmIntValues, myParmsUndoInfo.parmIntValues, prParmIntValueCount); // Get parameter float values. prParmFloatValues = new float[prParmFloatValueCount]; HoudiniAssetUtility.getArray1Id( prControl.prNodeId, HoudiniHost.getParmFloatValues, prParmFloatValues, prParmFloatValueCount); myParmsUndoInfo.parmFloatValues = new float[prParmFloatValueCount]; Array.Copy(prParmFloatValues, myParmsUndoInfo.parmFloatValues, prParmFloatValueCount); // Get parameter string (handle) values. prParmStringHandles = new int[prParmStringValueCount]; myParmStringValues = new string[prParmStringValueCount]; HoudiniAssetUtility.getArray1Id( prControl.prNodeId, HoudiniHost.getParmStringValues, prParmStringHandles, prParmStringValueCount); // Get parameter choice lists. prParmChoiceLists = new HAPI_ParmChoiceInfo[prParmChoiceCount]; myParmChoiceInfoStrings = new HAPI_ParmChoiceInfoStrings[prParmChoiceCount]; HoudiniAssetUtility.getArray1Id( prControl.prNodeId, HoudiniHost.getParmChoiceLists, prParmChoiceLists, prParmChoiceCount); cacheStringsFromHost(); // Go through parameters and set index map and multiparm map for undo info myParmsUndoInfo.parmNames.Clear(); myParmsUndoInfo.parmIndices.Clear(); foreach (HAPI_ParmInfo parm in prParms) { if (parm.isNode() && parm.id >= myParmInputs.Length) { // For input node params, make sure they have an entry // in myParmInputs for their data. Fixes bug 85804 setChangedNodeParameterIntoHost(parm.id); } // Need to check the index values are greater or equal to 0 // for now because there is a bug where some parameters are // being set to have an integer parameter type, a size of // zero, and an index value of -1 if (parm.isInt() && parm.intValuesIndex >= 0) { myParmsUndoInfo.parmIndices.Add(parm.intValuesIndex); } else if (parm.isFloat() && parm.floatValuesIndex >= 0) { myParmsUndoInfo.parmIndices.Add(parm.floatValuesIndex); } else if (parm.isString() && parm.stringValuesIndex >= 0) { myParmsUndoInfo.parmIndices.Add(parm.stringValuesIndex); } else { continue; } myParmsUndoInfo.parmNames.Add(parm.name); } }
/// <summary> /// Applies a Houdini's UVLayout node to each given gameobject's mesh data, and generates the output. /// The output could be a copy gameobject, or replace the mesh and materials on the original. /// </summary> /// <param name="gameObjects">Array of gameobjects containing meshes</param> /// <param name="outputMode">How the outputs should be generated</param> /// <param name="output_name_suffix">Name to append at end of each generated gameobject if outputMode == COPY</param> public static void ApplyUVLayoutTo(GameObject[] gameObjects, OutputMode outputMode, string output_name_suffix) { // A Houdini Engine session is always required. This should catch any licensing and installation issues. HEU_SessionBase session = HEU_SessionManager.GetOrCreateDefaultSession(); if (session == null || !session.IsSessionValid()) { Debug.LogError("Failed to get Houdini Engine session. Unable to apply UV layout."); return; } if (gameObjects == null || gameObjects.Length == 0) { Debug.LogError("No input objects found to apply UV layout."); return; } // For each gameobject with mesh: // -create an input node in the Houdini session // -import the mesh data into the input node // -connect input node to a new UVLayout node // -cook the UVLayout node // -generate the output mesh foreach (GameObject currentGO in gameObjects) { // Process the current gameobject to get the potential list of input mesh data. // HEU_InputUtility contains helper functions for uploading mesh data into Houdini. // Also handles LOD meshes. bool bHasLODGroup = false; HEU_InputInterfaceMesh inputMeshInterface = HEU_InputUtility.GetInputInterfaceByType(typeof(HEU_InputInterfaceMesh)) as HEU_InputInterfaceMesh; HEU_InputInterfaceMesh.HEU_InputDataMeshes inputMeshes = inputMeshInterface.GenerateMeshDatasFromGameObject(currentGO); if (inputMeshes == null || inputMeshes._inputMeshes.Count == 0) { Debug.LogWarningFormat("Failed to generate input mesh data for: {0}", currentGO.name); continue; } // Create the input node in Houdini. // Houdini Engine automatically creates a new object to contain the input node. string inputName = null; HAPI_NodeId inputNodeID = HEU_Defines.HEU_INVALID_NODE_ID; session.CreateInputNode(out inputNodeID, inputName); if (inputNodeID == HEU_Defines.HEU_INVALID_NODE_ID || !HEU_HAPIUtility.IsNodeValidInHoudini(session, inputNodeID)) { Debug.LogErrorFormat("Failed to create new input node in Houdini session!"); break; } // Need the HAPI_NodeInfo of the new input node to get its details, such as parent object ID. HAPI_NodeInfo nodeInfo = new HAPI_NodeInfo(); if (!session.GetNodeInfo(inputNodeID, ref nodeInfo)) { break; } // Cook the node to make sure everything is alright. if (!session.CookNode(inputNodeID, false)) { session.DeleteNode(nodeInfo.parentId); Debug.LogErrorFormat("New input node failed to cook!"); break; } // Now upload the mesh data into the input node. if (!inputMeshInterface.UploadData(session, inputNodeID, inputMeshes)) { session.DeleteNode(nodeInfo.parentId); Debug.LogErrorFormat("Failed to upload input mesh data"); break; } // Create UVLayout node in Houdini. Passing in the input node's parent node's ID will // create it within the same object as the input node.. HAPI_NodeId uvlayoutID = -1; if (!session.CreateNode(nodeInfo.parentId, "uvlayout", "UVLayout", true, out uvlayoutID)) { session.DeleteNode(nodeInfo.parentId); break; } // Example showing how to set the parameter on the new UVLayout node. session.SetParamIntValue(uvlayoutID, "correctareas", 0, 1); // Connect the input node to the UVLayout node. // Important bit here is the node IDs being passed in. if (!session.ConnectNodeInput(uvlayoutID, 0, inputNodeID, 0)) { session.DeleteNode(nodeInfo.parentId); break; } // Force cook the UVLayout node in Houdini. if (!HEU_HAPIUtility.CookNodeInHoudini(session, uvlayoutID, true, "uvlayout")) { session.DeleteNode(nodeInfo.parentId); break; } // Now its time to generate the actual output in Unity. A couple of utlity classes will help here. // materialCache will contain the list of materials generated.. List <HEU_MaterialData> materialCache = new List <HEU_MaterialData>(); // Suggested name of the folder within this project where output files might be written out to (eg. materials). string assetCachePathName = "uvlayoutcache"; // First create a HEU_GenerateGeoCache which will contain the geometry data from Houdiini. // This will get all the geometry data buffers from Houdini from the UVLayout node, along with the materials (new or existing). HEU_GenerateGeoCache geoCache = HEU_GenerateGeoCache.GetPopulatedGeoCache(session, inputNodeID, uvlayoutID, 0, bHasLODGroup, materialCache, assetCachePathName); if (geoCache == null) { session.DeleteNode(nodeInfo.parentId); break; } // Store reorganized data buffers into mesh groups. Groups are created if its a LOD mesh. List <HEU_GeoGroup> LODGroupMeshes = null; // The default material identifier (used when no material is supplied initially in Unity). int defaultMaterialKey = 0; // Flag whether to generate UVs, tangents, normals in Unity (in case they weren't created in Houdini). bool bGenerateUVs = false; bool bGenerateTangents = false; bool bGenerateNormals = false; bool bPartInstanced = false; // Now reorganize the data buffers into Unity mesh friendly format. // This handles point splitting into vertices, collider groups, submeshes based on multiple materials, LOD groups. // Can instead use HEU_GenerateGeoCache.GenerateGeoGroupUsingGeoCachePoints to keep as points instead. bool bResult = HEU_GenerateGeoCache.GenerateGeoGroupUsingGeoCacheVertices(session, geoCache, bGenerateUVs, bGenerateTangents, bGenerateNormals, bHasLODGroup, bPartInstanced, out LODGroupMeshes, out defaultMaterialKey); if (!bResult) { session.DeleteNode(nodeInfo.parentId); break; } // This will hold the output gameobject, along with any children and materials. HEU_GeneratedOutput generatedOutput = new HEU_GeneratedOutput(); if (outputMode == OutputMode.COPY) { // For copy mode, create and set new gameobject as output generatedOutput._outputData._gameObject = new GameObject(currentGO.name + "_HEU_modified"); } else if (outputMode == OutputMode.REPLACE) { // For replace, just use current input gameobject generatedOutput._outputData._gameObject = currentGO; } // Now generate the Unity meshes with material assignment. Handle LOD groups. int numLODs = LODGroupMeshes != null ? LODGroupMeshes.Count : 0; if (numLODs > 1) { bResult = HEU_GenerateGeoCache.GenerateLODMeshesFromGeoGroups(session, LODGroupMeshes, geoCache, generatedOutput, defaultMaterialKey, bGenerateUVs, bGenerateTangents, bGenerateNormals, bPartInstanced); } else if (numLODs == 1) { bResult = HEU_GenerateGeoCache.GenerateMeshFromSingleGroup(session, LODGroupMeshes[0], geoCache, generatedOutput, defaultMaterialKey, bGenerateUVs, bGenerateTangents, bGenerateNormals, bPartInstanced); } // Clean up by deleting the object node containing the input and uvlayout node. session.DeleteNode(nodeInfo.parentId); } }
public static HAPI_NodeId[] getNodeNetworkChildren( HAPI_NodeId network_node_id ) { #if ( UNITY_STANDALONE_WIN || UNITY_STANDALONE_OSX || ( UNITY_METRO && UNITY_EDITOR ) ) HAPI_NodeInfo node_info = new HAPI_NodeInfo(); HAPI_Result status_code = HAPI_GetNodeInfo( ref mySession, network_node_id, ref node_info ); processStatusCode( status_code ); HAPI_NodeId[] children = new HAPI_NodeId[ node_info.childNodeCount ]; status_code = HAPI_GetNodeNetworkChildren( ref mySession, network_node_id, children, node_info.childNodeCount ); processStatusCode( status_code ); return children; #else throw new HoudiniErrorUnsupportedPlatform(); #endif }
// NODES ---------------------------------------------------------------------------------------------------- public static HAPI_NodeInfo getNodeInfo( HAPI_NodeId node_id ) { #if ( UNITY_STANDALONE_WIN || UNITY_STANDALONE_OSX || ( UNITY_METRO && UNITY_EDITOR ) ) HAPI_NodeInfo node_info = new HAPI_NodeInfo(); HAPI_Result status_code = HAPI_GetNodeInfo( ref mySession, node_id, ref node_info ); processStatusCode( status_code ); return node_info; #else throw new HoudiniErrorUnsupportedPlatform(); #endif }
HAPI_GetNodeInfo( ref HAPI_Session session, HAPI_NodeId node_id, ref HAPI_NodeInfo node_info );
HAPI_GetNodeInfo(HAPI_NodeId node_id, ref HAPI_NodeInfo node_info);
HAPI_GetNodeInfo(ref HAPI_Session session, HAPI_NodeId node_id, ref HAPI_NodeInfo node_info);
private static extern HAPI_Result HAPI_GetNodeInfo( ref HAPI_Session session, HAPI_NodeId node_id, ref HAPI_NodeInfo node_info );