/// <summary> /// Returns the DetailLayer values for the given heightfield part. /// Converts from HF float[] values into int[,] array. /// </summary> /// <param name="session">Houdini Engine session to query</param> /// <param name="geoID">The geometry ID in Houdini</param> /// <param name="partID">The part ID in Houdini</param> /// <param name="detailResolution">Out value specifying the detail resolution acquired /// from the heightfield size.</param> /// <returns>The DetailLayer values</returns> public static int[,] GetDetailMapFromPart(HEU_SessionBase session, HAPI_NodeId geoID, HAPI_PartId partID, out int detailResolution) { detailResolution = 0; HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); bool bResult = session.GetVolumeInfo(geoID, partID, ref volumeInfo); if (!bResult) { return null; } int volumeXLength = volumeInfo.xLength; int volumeYLength = volumeInfo.yLength; // Unity requires square size if (volumeXLength != volumeYLength) { Debug.LogErrorFormat("Detail layer size must be square. Got {0}x{1} instead. Unable to apply detail layer.", volumeXLength, volumeYLength); return null; } // Use the size of the volume as the detail resolution size detailResolution = volumeXLength; // Number of heightfield values int totalHeightValues = volumeXLength * volumeYLength; // Get the floating point values from the heightfield float[] heightValues = new float[totalHeightValues]; bResult = HEU_GeneralUtility.GetArray2Arg(geoID, partID, session.GetHeightFieldData, heightValues, 0, totalHeightValues); if (!bResult) { return null; } // Convert float[] to int[,]. No conversion or scaling done since just want to use values as they are. int[,] intMap = new int[volumeXLength, volumeYLength]; for (int y = 0; y < volumeYLength; ++y) { for (int x = 0; x < volumeXLength; ++x) { float f = heightValues[x + y * volumeXLength]; // Flip for right-hand to left-handed coordinate system int ix = x; int iy = volumeYLength - (y + 1); intMap[ix, iy] = (int)f; } } return intMap; }
public void UpdateLayerFromPart(HEU_SessionBase session, HEU_PartData part) { HEU_GeoNode geoNode = part.ParentGeoNode; HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); bool bResult = session.GetVolumeInfo(geoNode.GeoID, part.PartID, ref volumeInfo); if (!bResult || volumeInfo.tupleSize != 1 || volumeInfo.zLength != 1 || volumeInfo.storage != HAPI_StorageType.HAPI_STORAGETYPE_FLOAT) { return; } string volumeName = HEU_SessionManager.GetString(volumeInfo.nameSH, session); part.SetVolumeLayerName(volumeName); //Debug.LogFormat("Part name: {0}, GeoName: {1}, Volume Name: {2}, Display: {3}", part.PartName, geoNode.GeoName, volumeName, geoNode.Displayable); bool bHeightPart = volumeName.Equals("height"); HEU_VolumeLayer layer = GetLayer(volumeName); if (layer == null) { layer = new HEU_VolumeLayer(); layer._layerName = volumeName; if (bHeightPart) { _layers.Insert(0, layer); } else { _layers.Add(layer); } } layer._part = part; GetPartLayerAttributes(session, geoNode.GeoID, part.PartID, layer); if (!bHeightPart) { part.DestroyAllData(); } if (!_updatedLayers.Contains(layer)) { if (bHeightPart) { _updatedLayers.Insert(0, layer); } else { _updatedLayers.Add(layer); } } }
/// <summary> /// Helper to set heightfield data for a specific volume node. /// Used for a specific terrain layer. /// </summary> /// <param name="session">Session that the volume node resides in.</param> /// <param name="volumeNodeID">ID of the target volume node</param> /// <param name="partID">Part ID</param> /// <param name="heightValues">Array of height or alpha values</param> /// <param name="heightFieldName">Name of the layer</param> /// <returns>True if successfully uploaded heightfield values</returns> public bool SetHeightFieldData(HEU_SessionBase session, HAPI_NodeId volumeNodeID, HAPI_PartId partID, float[] heightValues, string heightFieldName, ref HAPI_VolumeInfo baseVolumeInfo) { // Cook the node to get infos below if (!session.CookNode(volumeNodeID, false)) { return(false); } // Get Geo, Part, and Volume infos HAPI_GeoInfo geoInfo = new HAPI_GeoInfo(); if (!session.GetGeoInfo(volumeNodeID, ref geoInfo)) { return(false); } HAPI_PartInfo partInfo = new HAPI_PartInfo(); if (!session.GetPartInfo(geoInfo.nodeId, partID, ref partInfo)) { return(false); } HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); if (!session.GetVolumeInfo(volumeNodeID, partInfo.id, ref volumeInfo)) { return(false); } volumeInfo.tileSize = 1; // Use same transform as base layer volumeInfo.transform = baseVolumeInfo.transform; if (!session.SetVolumeInfo(volumeNodeID, partInfo.id, ref volumeInfo)) { Debug.LogError("Unable to set volume info on input heightfield node!"); return(false); } // Now set the height data if (!session.SetHeightFieldData(geoInfo.nodeId, partInfo.id, heightFieldName, heightValues, 0, heightValues.Length)) { Debug.LogErrorFormat("Unable to set `{0}` height values on input heightfield node!\n" + "Check your terrain sizes including Control Texture Resolution is less than the Heightmap Resolution.", heightFieldName); return(false); } return(true); }
private void UpdateVolumeLayers(HEU_SessionBase session, HEU_HoudiniAsset houdiniAsset, List<HEU_PartData> volumeParts) { bool bResult; foreach (HEU_PartData part in volumeParts) { HEU_GeoNode geoNode = part.ParentGeoNode; HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); bResult = session.GetVolumeInfo(geoNode.GeoID, part.PartID, ref volumeInfo); if (!bResult || volumeInfo.tupleSize != 1 || volumeInfo.zLength != 1 || volumeInfo.storage != HAPI_StorageType.HAPI_STORAGETYPE_FLOAT) { continue; } string volumeName = HEU_SessionManager.GetString(volumeInfo.nameSH, session); part.SetVolumeLayerName(volumeName); //Debug.LogFormat("Part name: {0}, GeoName: {1}, Volume Name: {2}, Display: {3}", part.PartName, geoNode.GeoName, volumeName, geoNode.Displayable); bool bHeightPart = volumeName.Equals("height"); HEU_VolumeLayer layer = GetLayer(volumeName); if (layer == null) { layer = new HEU_VolumeLayer(); layer._layerName = volumeName; layer._splatTexture = LoadDefaultSplatTexture(); if (bHeightPart) { _layers.Insert(0, layer); } else { _layers.Add(layer); } } layer._part = part; if (!bHeightPart) { part.DestroyAllData(); } } }
/// <summary> /// Helper to set heightfield data for a specific volume node. /// Used for a specific terrain layer. /// </summary> /// <param name="session">Session that the volume node resides in.</param> /// <param name="volumeNodeID">ID of the target volume node</param> /// <param name="partID">Part ID</param> /// <param name="heightValues">Array of height or alpha values</param> /// <param name="heightFieldName">Name of the layer</param> /// <returns>True if successfully uploaded heightfield values</returns> public bool SetHeightFieldData(HEU_SessionBase session, HAPI_NodeId volumeNodeID, HAPI_PartId partID, float[] heightValues, string heightFieldName) { // Cook the node to get infos below if (!session.CookNode(volumeNodeID, false)) { return false; } // Get Geo, Part, and Volume infos HAPI_GeoInfo geoInfo = new HAPI_GeoInfo(); if (!session.GetGeoInfo(volumeNodeID, ref geoInfo)) { return false; } HAPI_PartInfo partInfo = new HAPI_PartInfo(); if (!session.GetPartInfo(geoInfo.nodeId, partID, ref partInfo)) { return false; } HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); if (!session.GetVolumeInfo(volumeNodeID, partInfo.id, ref volumeInfo)) { return false; } volumeInfo.tileSize = 1; if (!session.SetVolumeInfo(volumeNodeID, partInfo.id, ref volumeInfo)) { Debug.LogError("Unable to set volume info on input heightfield node!"); return false; } // Now set the height data if (!session.SetHeightFieldData(geoInfo.nodeId, partInfo.id, heightFieldName, heightValues, 0, heightValues.Length)) { Debug.LogError("Unable to set height values on input heightfield node!"); return false; } return true; }
private bool SetMaskLayer(HEU_SessionBase session, HEU_InputDataTerrain idt, ref HAPI_VolumeInfo baseVolumeInfo) { int sizeX = idt._terrainData.alphamapWidth; int sizeY = idt._terrainData.alphamapHeight; int totalSize = sizeX * sizeY; float[] maskValues = new float[totalSize]; if (!SetHeightFieldData(session, idt._maskNodeID, 0, maskValues, HEU_Defines.HAPI_HEIGHTFIELD_LAYERNAME_MASK, ref baseVolumeInfo)) { return(false); } if (!session.CommitGeo(idt._maskNodeID)) { Debug.LogError("Failed to commit volume layer 'mask'"); return(false); } return(true); }
private void ParseVolumeDatas(HEU_SessionBase session, List<HEU_PartData> volumeParts) { bool bResult; foreach (HEU_PartData part in volumeParts) { HEU_GeoNode geoNode = part.ParentGeoNode; HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); bResult = session.GetVolumeInfo(geoNode.GeoID, part.PartID, ref volumeInfo); if(!bResult || volumeInfo.tupleSize != 1 || volumeInfo.zLength != 1 || volumeInfo.storage != HAPI_StorageType.HAPI_STORAGETYPE_FLOAT) { continue; } string volumeName = HEU_SessionManager.GetString(volumeInfo.nameSH, session); //Debug.LogFormat("Part name: {0}, GeoName: {1}, Volume Name: {2}, Display: {3}", part.PartName, geoNode.GeoName, volumeName, geoNode.Displayable); if(volumeName.Equals("height")) { if (_heightMapVolumeData == null) { _heightMapVolumeData = new HEU_VolumeData(); _heightMapVolumeData._partData = part; _heightMapVolumeData._volumeInfo = volumeInfo; } } else { HEU_VolumeData volumeData = new HEU_VolumeData(); volumeData._partData = part; volumeData._volumeInfo = volumeInfo; _textureVolumeDatas.Add(volumeData); } } }
/// <summary> /// Retrieves the heightmap from Houdini for the given volume part, converts to Unity coordinates, /// normalizes to 0 and 1, along with min and max height values, as well as the range. /// </summary> /// <param name="session">Current Houdini session</param> /// <param name="geoID">Geometry object ID</param> /// <param name="partID">The volume part ID</param> /// <param name="heightMapSize">Size of each dimension of the heightmap (assumes equal sides).</param> /// <param name="minHeight">Found minimum height value in the heightmap.</param> /// <param name="maxHeight">Found maximum height value in the heightmap.</param> /// <param name="heightRange">Found height range in the heightmap.</param> /// <returns>The converted heightmap from Houdini.</returns> public static float[] GetNormalizedHeightmapFromPartWithMinMax(HEU_SessionBase session, HAPI_NodeId geoID, HAPI_PartId partID, int heightMapWidth, int heightMapHeight, ref float minHeight, ref float maxHeight, ref float heightRange, bool bUseHeightRangeOverride) { minHeight = float.MaxValue; maxHeight = float.MinValue; heightRange = 1; HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); bool bResult = session.GetVolumeInfo(geoID, partID, ref volumeInfo); if (!bResult) { return null; } int volumeXLength = volumeInfo.xLength; int volumeYLength = volumeInfo.yLength; // Number of heightfield values int totalHeightValues = volumeXLength * volumeYLength; float[] heightValues = new float[totalHeightValues]; if (!GetHeightmapFromPart(session, volumeXLength, volumeYLength, geoID, partID, ref heightValues, ref minHeight, ref maxHeight)) { return null; } heightRange = (maxHeight - minHeight); // Use the override height range if user has set via attribute bool bHeightRangeOverriden = false; if (bUseHeightRangeOverride) { float userHeightRange = GetHeightRangeFromHeightfield(session, geoID, partID); if (userHeightRange > 0) { heightRange = userHeightRange; bHeightRangeOverriden = true; } } if (heightRange == 0f) { // Always use a non-zero height range, otherwise user can't paint height on Terrain. heightRange = 1f; } //Debug.LogFormat("{0} : {1}", HEU_SessionManager.GetString(volumeInfo.nameSH, session), heightRange); const int UNITY_MAX_HEIGHT_RANGE = 65536; if (Mathf.RoundToInt(heightRange) > UNITY_MAX_HEIGHT_RANGE) { Debug.LogWarningFormat("Unity Terrain has maximum height range of {0}. This HDA height range is {1}, so it will be maxed out at {0}.\nPlease resize to within valid range!", UNITY_MAX_HEIGHT_RANGE, Mathf.RoundToInt(heightRange)); heightRange = UNITY_MAX_HEIGHT_RANGE; } // Remap height values to fit terrain size int paddingWidth = heightMapWidth - volumeXLength; int paddingLeft = Mathf.CeilToInt(paddingWidth * 0.5f); int paddingRight = heightMapWidth - paddingLeft; //Debug.LogFormat("Padding: Width={0}, Left={1}, Right={2}", paddingWidth, paddingLeft, paddingRight); int paddingHeight = heightMapHeight - volumeYLength; int paddingTop = Mathf.CeilToInt(paddingHeight * 0.5f); int paddingBottom = heightMapHeight - paddingTop; //Debug.LogFormat("Padding: Height={0}, Top={1}, Bottom={2}", paddingHeight, paddingTop, paddingBottom); // Normalize the height values into the range between 0 and 1, inclusive. float inverseHeightRange = 1f / heightRange; float normalizeMinHeight = minHeight; if (minHeight >= 0f && minHeight <= 1f && maxHeight >= 0f && maxHeight <= 1f) { // Its important to leave the values alone if they are already normalized. // So these values don't actually do anything in the normalization calculation below. inverseHeightRange = 1f; normalizeMinHeight = 0f; } // Set height values at centre of the terrain, with padding on the sides if we resized float[] resizedHeightValues = new float[heightMapWidth * heightMapHeight]; for (int y = 0; y < heightMapHeight; ++y) { for (int x = 0; x < heightMapWidth; ++x) { if (y >= paddingTop && y < (paddingBottom) && x >= paddingLeft && x < (paddingRight)) { int ay = x - paddingLeft; int ax = y - paddingTop; float f = heightValues[ay + ax * volumeXLength]; if (!bHeightRangeOverriden) { f -= normalizeMinHeight; } f *= inverseHeightRange; // Flip for right-hand to left-handed coordinate system int ix = x; int iy = heightMapHeight - (y + 1); // Unity expects height array indexing to be [y, x]. resizedHeightValues[iy + ix * heightMapWidth] = f; } } } return resizedHeightValues; }
/// <summary> /// Upload the base height layer into heightfield network. /// </summary> /// <param name="session"></param> /// <param name="idt"></param> /// <returns></returns> public bool UploadHeightValuesWithTransform(HEU_SessionBase session, HEU_InputDataTerrain idt) { // Get Geo, Part, and Volume infos HAPI_GeoInfo geoInfo = new HAPI_GeoInfo(); if (!session.GetGeoInfo(idt._heightNodeID, ref geoInfo)) { Debug.LogError("Unable to get geo info from heightfield node!"); return false; } HAPI_PartInfo partInfo = new HAPI_PartInfo(); if (!session.GetPartInfo(geoInfo.nodeId, 0, ref partInfo)) { Debug.LogError("Unable to get part info from heightfield node!"); return false; } HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); if (!session.GetVolumeInfo(idt._heightNodeID, 0, ref volumeInfo)) { Debug.LogError("Unable to get volume info from heightfield node!"); return false; } if (volumeInfo.xLength != Mathf.RoundToInt(idt._numPointsX / idt._voxelSize) || volumeInfo.yLength != Mathf.RoundToInt(idt._numPointsY / idt._voxelSize) || idt._terrainData.heightmapResolution != volumeInfo.xLength || idt._terrainData.heightmapResolution != volumeInfo.yLength) { Debug.LogError("Created heightfield in Houdini differs in voxel size from input terrain!"); return false; } // Update volume infos, and set it. This is required. volumeInfo.tileSize = 1; volumeInfo.type = HAPI_VolumeType.HAPI_VOLUMETYPE_HOUDINI; volumeInfo.storage = HAPI_StorageType.HAPI_STORAGETYPE_FLOAT; volumeInfo.transform = idt._transform; volumeInfo.minX = 0; volumeInfo.minY = 0; volumeInfo.minZ = 0; volumeInfo.tupleSize = 1; volumeInfo.tileSize = 1; volumeInfo.hasTaper = false; volumeInfo.xTaper = 0f; volumeInfo.yTaper = 0f; if (!session.SetVolumeInfo(idt._heightNodeID, partInfo.id, ref volumeInfo)) { Debug.LogError("Unable to set volume info on input heightfield node!"); return false; } // Now set the height data float[,] heights = idt._terrainData.GetHeights(0, 0, volumeInfo.xLength, volumeInfo.yLength); int sizeX = heights.GetLength(0); int sizeY = heights.GetLength(1); int totalSize = sizeX * sizeY; // Convert to single array float[] heightsArr = new float[totalSize]; for (int j = 0; j < sizeY; j++) { for (int i = 0; i < sizeX; i++) { // Flip for coordinate system change float h = heights[i, (sizeY - j - 1)]; heightsArr[i + j * sizeX] = h * idt._heightScale; } } // Set the base height layer if (!session.SetHeightFieldData(idt._heightNodeID, 0, "height", heightsArr, 0, totalSize)) { Debug.LogError("Unable to set height values on input heightfield node!"); return false; } if (!session.CommitGeo(idt._heightNodeID)) { Debug.LogError("Unable to commit geo on input heightfield node!"); return false; } return true; }
/// <summary> /// Upload the base height layer into heightfield network. /// </summary> /// <param name="session"></param> /// <param name="idt"></param> /// <returns></returns> public bool UploadHeightValuesWithTransform(HEU_SessionBase session, HEU_InputDataTerrain idt, ref HAPI_VolumeInfo volumeInfo) { // Get Geo, Part, and Volume infos HAPI_GeoInfo geoInfo = new HAPI_GeoInfo(); if (!session.GetGeoInfo(idt._heightNodeID, ref geoInfo)) { HEU_Logger.LogError("Unable to get geo info from heightfield node!"); return(false); } HAPI_PartInfo partInfo = new HAPI_PartInfo(); if (!session.GetPartInfo(geoInfo.nodeId, 0, ref partInfo)) { HEU_Logger.LogError("Unable to get part info from heightfield node!"); return(false); } volumeInfo = new HAPI_VolumeInfo(); if (!session.GetVolumeInfo(idt._heightNodeID, 0, ref volumeInfo)) { HEU_Logger.LogError("Unable to get volume info from heightfield node!"); return(false); } if ((volumeInfo.xLength - 1) != Mathf.RoundToInt(idt._numPointsX / idt._voxelSize) || (volumeInfo.yLength - 1) != Mathf.RoundToInt(idt._numPointsY / idt._voxelSize) || idt._terrainData.heightmapResolution != volumeInfo.xLength || idt._terrainData.heightmapResolution != volumeInfo.yLength) { HEU_Logger.LogWarning("Created heightfield in Houdini differs in voxel size from input terrain! Terrain may require resampling."); } // Update volume infos, and set it. This is required. volumeInfo.tileSize = 1; volumeInfo.type = HAPI_VolumeType.HAPI_VOLUMETYPE_HOUDINI; volumeInfo.storage = HAPI_StorageType.HAPI_STORAGETYPE_FLOAT; volumeInfo.transform = idt._transform; volumeInfo.minX = 0; volumeInfo.minY = 0; volumeInfo.minZ = 0; volumeInfo.tupleSize = 1; volumeInfo.tileSize = 1; volumeInfo.hasTaper = false; volumeInfo.xTaper = 0f; volumeInfo.yTaper = 0f; if (!session.SetVolumeInfo(idt._heightNodeID, partInfo.id, ref volumeInfo)) { HEU_Logger.LogError("Unable to set volume info on input heightfield node!"); return(false); } // Now set the height data float[,] heights = idt._terrainData.GetHeights(0, 0, idt._terrainData.heightmapResolution, idt._terrainData.heightmapResolution); int sizeX = heights.GetLength(0); int sizeY = heights.GetLength(1); int totalSize = sizeX * sizeY; // Convert to single array float[] heightsArr = new float[totalSize]; for (int j = 0; j < sizeY; j++) { for (int i = 0; i < sizeX; i++) { // Flip for coordinate system change float h = heights[i, (sizeY - j - 1)]; heightsArr[i + j * sizeX] = h * idt._heightScale; } } if (volumeInfo.xLength != volumeInfo.yLength) { HEU_Logger.LogError("Error: Houdini heightmap must be square!"); return(false); } if (idt._terrainData.heightmapResolution != volumeInfo.xLength) { // Resize heightsArr to idt._terrainData.heightmapResolution HEU_Logger.LogWarningFormat("Attempting to resize landscape from ({0}x{1}) to ({2}x{3})", idt._terrainData.heightmapResolution, idt._terrainData.heightmapResolution, volumeInfo.xLength, volumeInfo.xLength); heightsArr = HEU_TerrainUtility.ResampleData(heightsArr, idt._terrainData.heightmapResolution, idt._terrainData.heightmapResolution, volumeInfo.xLength, volumeInfo.xLength); sizeX = volumeInfo.xLength; sizeY = volumeInfo.yLength; totalSize = sizeX * sizeY; } // Set the base height layer if (!session.SetHeightFieldData(idt._heightNodeID, 0, HEU_Defines.HAPI_HEIGHTFIELD_LAYERNAME_HEIGHT, heightsArr, 0, totalSize)) { HEU_Logger.LogError("Unable to set height values on input heightfield node!"); return(false); } SetTerrainDataAttributesToHeightField(session, geoInfo.nodeId, 0, idt._terrainData); SetTreePrototypes(session, geoInfo.nodeId, 0, idt._terrainData); if (!session.CommitGeo(idt._heightNodeID)) { HEU_Logger.LogError("Unable to commit geo on input heightfield node!"); return(false); } return(true); }
public static bool GenerateTerrainFromVolume(HEU_SessionBase session, ref HAPI_VolumeInfo volumeInfo, HAPI_NodeId geoID, HAPI_PartId partID, GameObject gameObject, out TerrainData terrainData, out Vector3 volumePositionOffset) { terrainData = null; volumePositionOffset = Vector3.zero; if (volumeInfo.zLength == 1 && volumeInfo.tupleSize == 1) { // Heightfields will be converted to terrain in Unity. // Unity requires terrainData.heightmapResolution to be square power of two plus 1 (eg. 513, 257, 129, 65). // Houdini gives volumeInfo.xLength and volumeInfo.yLength which are the number of height values per dimension. // Note that volumeInfo.xLength and volumeInfo.yLength is equal to Houdini heightfield size / grid spacing. // The heightfield grid spacing is given as volumeTransformMatrix.scale but divided by 2 (grid spacing / 2 = volumeTransformMatrix.scale). // It is recommended to use grid spacing of 2. // Use the volumeInfo.transform to get the actual heightfield position and size. Matrix4x4 volumeTransformMatrix = HEU_HAPIUtility.GetMatrixFromHAPITransform(ref volumeInfo.transform, false); Vector3 position = HEU_HAPIUtility.GetPosition(ref volumeTransformMatrix); Vector3 scale = HEU_HAPIUtility.GetScale(ref volumeTransformMatrix); // Calculate real terrain size in both Houdini and Unity. // The height values will be mapped over this terrain size. float gridSpacingX = scale.x * 2f; float gridSpacingY = scale.y * 2f; float terrainSizeX = Mathf.Round((volumeInfo.xLength - 1) * gridSpacingX); float terrainSizeY = Mathf.Round((volumeInfo.yLength - 1) * gridSpacingY); //Debug.LogFormat("GS = {0},{1},{2}. SX = {1}. SY = {2}", gridSpacingX, gridSpacingY, terrainSizeX, terrainSizeY); //Debug.LogFormat("HeightField Pos:{0}, Scale:{1}", position, scale.ToString("{0.00}")); //Debug.LogFormat("HeightField tileSize:{0}, xLength:{1}, yLength:{2}", volumeInfo.tileSize.ToString("{0.00}"), volumeInfo.xLength.ToString("{0.00}"), volumeInfo.yLength.ToString("{0.00}")); //Debug.LogFormat("HeightField Terrain Size x:{0}, y:{1}", terrainSizeX.ToString("{0.00}"), terrainSizeY.ToString("{0.00}")); //Debug.LogFormat("HeightField minX={0}, minY={1}, minZ={2}", volumeInfo.minX.ToString("{0.00}"), volumeInfo.minY.ToString("{0.00}"), volumeInfo.minZ.ToString("{0.00}")); const int UNITY_MINIMUM_HEIGHTMAP_RESOLUTION = 33; if (terrainSizeX < UNITY_MINIMUM_HEIGHTMAP_RESOLUTION || terrainSizeY < UNITY_MINIMUM_HEIGHTMAP_RESOLUTION) { Debug.LogWarningFormat("Unity Terrain has a minimum heightmap resolution of {0}. This HDA heightmap size is {1}x{2}." + "\nPlease resize the terrain to a value higher than this.", UNITY_MINIMUM_HEIGHTMAP_RESOLUTION, terrainSizeX, terrainSizeY); return false; } Terrain terrain = HEU_GeneralUtility.GetOrCreateComponent<Terrain>(gameObject); TerrainCollider collider = HEU_GeneralUtility.GetOrCreateComponent<TerrainCollider>(gameObject); if (terrain.terrainData == null) { terrain.terrainData = new TerrainData(); } terrainData = terrain.terrainData; collider.terrainData = terrainData; // Heightmap resolution must be square power-of-two plus 1. // Unity will automatically resize terrainData.heightmapResolution so need to handle the changed size (if Unity changed it). int heightMapResolution = volumeInfo.xLength; terrainData.heightmapResolution = heightMapResolution; int terrainResizedDelta = terrainData.heightmapResolution - heightMapResolution; if (terrainResizedDelta < 0) { Debug.LogWarningFormat("Note that Unity automatically resized terrain resolution to {0} from {1}. Use terrain size of power of two plus 1, and grid spacing of 2.", heightMapResolution, terrainData.heightmapResolution); heightMapResolution = terrainData.heightmapResolution; } else if(terrainResizedDelta > 0) { Debug.LogErrorFormat("Unsupported terrain size. Use terrain size of power of two plus 1, and grid spacing of 2. (delta = {0})", terrainResizedDelta); return false; } // Get the height values from Houdini and find the min and max height range. float minHeight = 0; float maxHeight = 0; float[] heightValues = null; if (!GetHeightfieldValues(session, volumeInfo.xLength, volumeInfo.yLength, geoID, partID, ref heightValues, ref minHeight, ref maxHeight)) { return false; } const int UNITY_MAX_HEIGHT_RANGE = 65536; float heightRange = (maxHeight - minHeight); if (Mathf.RoundToInt(heightRange) > UNITY_MAX_HEIGHT_RANGE) { Debug.LogWarningFormat("Unity Terrain has maximum height range of {0}. This HDA height range is {1}, so it will be maxed out at {0}.\nPlease resize to within valid range!", UNITY_MAX_HEIGHT_RANGE, Mathf.RoundToInt(heightRange)); heightRange = UNITY_MAX_HEIGHT_RANGE; } int mapWidth = volumeInfo.xLength; int mapHeight = volumeInfo.yLength; int paddingWidth = heightMapResolution - mapWidth; int paddingLeft = Mathf.CeilToInt(paddingWidth * 0.5f); int paddingRight = heightMapResolution - paddingLeft; //Debug.LogFormat("Padding: Width={0}, Left={1}, Right={2}", paddingWidth, paddingLeft, paddingRight); int paddingHeight = heightMapResolution - mapHeight; int paddingTop = Mathf.CeilToInt(paddingHeight * 0.5f); int paddingBottom = heightMapResolution - paddingTop; //Debug.LogFormat("Padding: Height={0}, Top={1}, Bottom={2}", paddingHeight, paddingTop, paddingBottom); // Set height values at centre of the terrain, with padding on the sides if we resized float[,] unityHeights = new float[heightMapResolution, heightMapResolution]; for (int y = 0; y < heightMapResolution; ++y) { for (int x = 0; x < heightMapResolution; ++x) { if (y >= paddingTop && y < (paddingBottom) && x >= paddingLeft && x < (paddingRight)) { int ay = x - paddingLeft; int ax = y - paddingTop; // Unity expects normalized height values float h = heightValues[ay + ax * mapWidth] - minHeight; float f = h / heightRange; // Flip for right-hand to left-handed coordinate system int ix = x; int iy = heightMapResolution - (y + 1); // Unity expects height array indexing to be [y, x]. unityHeights[ix, iy] = f; } } } terrainData.baseMapResolution = heightMapResolution; terrainData.alphamapResolution = heightMapResolution; //int detailResolution = heightMapResolution; // 128 is the maximum for resolutionPerPatch const int resolutionPerPatch = 128; terrainData.SetDetailResolution(resolutionPerPatch, resolutionPerPatch); // Note SetHeights must be called before setting size in next line, as otherwise // the internal terrain size will not change after setting the size. terrainData.SetHeights(0, 0, unityHeights); terrainData.size = new Vector3(terrainSizeX, heightRange, terrainSizeY); terrain.Flush(); // Unity Terrain has origin at bottom left, whereas Houdini uses centre of terrain. // Use volume bounds to set position offset when using split tiles float xmin, xmax, zmin, zmax, ymin, ymax, xcenter, ycenter, zcenter; session.GetVolumeBounds(geoID, partID, out xmin, out ymin, out zmin, out xmax, out ymax, out zmax, out xcenter, out ycenter, out zcenter); //Debug.LogFormat("xmin: {0}, xmax: {1}, ymin: {2}, ymax: {3}, zmin: {4}, zmax: {5}, xc: {6}, yc: {7}, zc: {8}", // xmin, xmax, ymin, ymax, zmin, zmax, xcenter, ycenter, zcenter); // Offset position is based on size of heightfield float offsetX = (float)heightMapResolution / (float)mapWidth; float offsetZ = (float)heightMapResolution / (float)mapHeight; //Debug.LogFormat("offsetX: {0}, offsetZ: {1}", offsetX, offsetZ); //Debug.LogFormat("position.x: {0}, position.z: {1}", position.x, position.z); //volumePositionOffset = new Vector3(-position.x * offsetX, minHeight + position.y, position.z * offsetZ); volumePositionOffset = new Vector3((terrainSizeX + xmin) * offsetX, minHeight + position.y, zmin * offsetZ); return true; } else { Debug.LogWarning("Non-heightfield volume type not supported!"); } return false; }
public void GenerateTerrainWithAlphamaps(HEU_SessionBase session, HEU_HoudiniAsset houdiniAsset, bool bRebuild) { if(_layers == null || _layers.Count == 0) { Debug.LogError("Unable to generate terrain due to lack of heightfield layers!"); return; } HEU_VolumeLayer heightLayer = _layers[0]; HAPI_VolumeInfo heightVolumeInfo = new HAPI_VolumeInfo(); bool bResult = session.GetVolumeInfo(_ownerNode.GeoID, heightLayer._part.PartID, ref heightVolumeInfo); if (!bResult) { Debug.LogErrorFormat("Unable to get volume info for height layer: {0}!", heightLayer._layerName); return; } // Special handling of volume cache presets. It is applied here (if exists) because it might pertain to TerrainData that exists // in the AssetDatabase. If we don't apply here but rather create a new one, the existing file will get overwritten. // Applying the preset here for terrain ensures the TerrainData is reused. // Get the volume preset for this part HEU_VolumeCachePreset volumeCachePreset = houdiniAsset.GetVolumeCachePreset(_ownerNode.ObjectNode.ObjectName, _ownerNode.GeoName, TileIndex); if (volumeCachePreset != null) { ApplyPreset(volumeCachePreset); // Remove it so that it doesn't get applied when doing the recook step houdiniAsset.RemoveVolumeCachePreset(volumeCachePreset); } // The TerrainData and TerrainLayer files needs to be saved out if we create them. This creates the relative folder // path from the Asset's cache folder: {assetCache}/{geo name}/Terrain/Tile{tileIndex}/... string relativeFolderPath = HEU_Platform.BuildPath(_ownerNode.GeoName, HEU_Defines.HEU_FOLDER_TERRAIN, HEU_Defines.HEU_FOLDER_TILE + TileIndex); if (bRebuild) { // For full rebuild, re-create the TerrainData instead of using previous _terrainData = null; } //Debug.Log("Generating Terrain with AlphaMaps: " + (_terrainData != null ? _terrainData.name : "NONE")); TerrainData terrainData = _terrainData; Vector3 terrainOffsetPosition = Vector3.zero; // Look up TerrainData file via attribute if user has set it string terrainDataFile = HEU_GeneralUtility.GetAttributeStringValueSingle(session, _ownerNode.GeoID, heightLayer._part.PartID, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_TERRAINDATA_FILE_ATTR, HAPI_AttributeOwner.HAPI_ATTROWNER_PRIM); if (!string.IsNullOrEmpty(terrainDataFile)) { TerrainData loadedTerrainData = HEU_AssetDatabase.LoadAssetAtPath(terrainDataFile, typeof(TerrainData)) as TerrainData; if (loadedTerrainData == null) { Debug.LogWarningFormat("TerrainData, set via attribute, not found at: {0}", terrainDataFile); } else { // In the case that the specified TerrainData belongs to another Terrain (i.e. input Terrain), // make a copy of it and store it in our cache. Note that this overwrites existing TerrainData in our cache // because the workflow is such that attributes will always override local setting. string bakedTerrainPath = houdiniAsset.GetValidAssetCacheFolderPath(); bakedTerrainPath = HEU_Platform.BuildPath(bakedTerrainPath, relativeFolderPath); terrainData = HEU_AssetDatabase.CopyAndLoadAssetAtAnyPath(loadedTerrainData, bakedTerrainPath, typeof(TerrainData), true) as TerrainData; if (terrainData == null) { Debug.LogErrorFormat("Unable to copy TerrainData from {0} for generating Terrain.", terrainDataFile); } } } // Generate the terrain and terrain data from the height layer. This applies height values. bResult = HEU_TerrainUtility.GenerateTerrainFromVolume(session, ref heightVolumeInfo, heightLayer._part.ParentGeoNode.GeoID, heightLayer._part.PartID, heightLayer._part.OutputGameObject, ref terrainData, out terrainOffsetPosition); if (!bResult || terrainData == null) { return; } if (_terrainData != terrainData) { _terrainData = terrainData; heightLayer._part.SetTerrainData(terrainData, relativeFolderPath); } heightLayer._part.SetTerrainOffsetPosition(terrainOffsetPosition); int terrainSize = terrainData.heightmapResolution; // Now process TerrainLayers and alpha maps // First, preprocess all layers to get heightfield arrays, converted to proper size List<float[]> heightFields = new List<float[]>(); // Corresponding list of HF volume layers to process as splatmaps List<HEU_VolumeLayer> volumeLayersToProcess = new List<HEU_VolumeLayer>(); int numLayers = _layers.Count; float minHeight = 0; float maxHeight = 0; float heightRange = 0; // This skips the height layer, and processes all other layers. // Note that mask shouldn't be part of _layers at this point. for(int i = 1; i < numLayers; ++i) { float[] normalizedHF = HEU_TerrainUtility.GetNormalizedHeightmapFromPartWithMinMax(session, _ownerNode.GeoID, _layers[i]._part.PartID, _layers[i]._xLength, _layers[i]._yLength, ref minHeight, ref maxHeight, ref heightRange); if (normalizedHF != null && normalizedHF.Length > 0) { heightFields.Add(normalizedHF); volumeLayersToProcess.Add(_layers[i]); } } int numVolumeLayers = volumeLayersToProcess.Count; HAPI_NodeId geoID; HAPI_PartId partID; Texture2D defaultTexture = LoadDefaultSplatTexture(); #if UNITY_2018_3_OR_NEWER // Create or update the terrain layers based on heightfield layers. // Keep existing TerrainLayers, and either update or append to them TerrainLayer[] existingTerrainLayers = terrainData.terrainLayers; // Total layers are existing layers + new alpha maps List<TerrainLayer> finalTerrainLayers = new List<TerrainLayer>(existingTerrainLayers); // This holds the alpha map indices for each layer that will be added to the TerrainData. // The alpha maps could be a mix of existing and new values, so need to know which to use // Initially set to use existing alpha maps, then override later on if specified via HF layers List<int> alphaMapIndices = new List<int>(); for (int a = 0; a < existingTerrainLayers.Length; ++a) { // Negative indices for existing alpha map (offset by -1) alphaMapIndices.Add(-a - 1); } bool bNewTerrainLayer = false; HEU_VolumeLayer layer = null; TerrainLayer terrainLayer = null; bool bSetTerrainLayerProperties = true; for (int m = 0; m < numVolumeLayers; ++m) { bNewTerrainLayer = false; bSetTerrainLayerProperties = true; layer = volumeLayersToProcess[m]; geoID = _ownerNode.GeoID; partID = layer._part.PartID; terrainLayer = null; int terrainLayerIndex = -1; // The TerrainLayer attribute overrides existing TerrainLayer. So if its set, load and use it. string terrainLayerFile = HEU_GeneralUtility.GetAttributeStringValueSingle(session, geoID, partID, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_TERRAINLAYER_FILE_ATTR, HAPI_AttributeOwner.HAPI_ATTROWNER_PRIM); if (!string.IsNullOrEmpty(terrainLayerFile)) { terrainLayer = HEU_AssetDatabase.LoadAssetAtPath(terrainLayerFile, typeof(TerrainLayer)) as TerrainLayer; if (terrainLayer == null) { Debug.LogWarningFormat("TerrainLayer, set via attribute, not found at: {0}", terrainLayerFile); // Not earlying out or skipping this layer due to error because we want to keep proper indexing // by creating a new TerrainLayer. } else { // TerrainLayer loaded from attribute. // It could be an existing TerrainLayer that is already part of finalTerrainLayers // or could be a new one which needs to be added. // If its a different TerrainLayer than existing, update the finalTerrainLayers, and index. if (layer._terrainLayer != null && layer._terrainLayer != terrainLayer) { terrainLayerIndex = HEU_TerrainUtility.GetTerrainLayerIndex(layer._terrainLayer, existingTerrainLayers); if (terrainLayerIndex >= 0) { finalTerrainLayers[terrainLayerIndex] = terrainLayer; } } if (terrainLayerIndex == -1) { // Always check if its part of existing list so as not to add it again terrainLayerIndex = HEU_TerrainUtility.GetTerrainLayerIndex(terrainLayer, existingTerrainLayers); } } } // No terrain layer specified, so try using existing if we have it if (terrainLayer == null) { terrainLayerIndex = HEU_TerrainUtility.GetTerrainLayerIndex(layer._terrainLayer, existingTerrainLayers); if (terrainLayerIndex >= 0) { // Note the terrainLayerIndex is same for finalTerrainLayers as existingTerrainLayers terrainLayer = existingTerrainLayers[terrainLayerIndex]; } } // Still not found, so just create a new one if (terrainLayer == null) { terrainLayer = new TerrainLayer(); terrainLayer.name = layer._layerName; //Debug.LogFormat("Created new TerrainLayer with name: {0} ", terrainLayer.name); bNewTerrainLayer = true; } if (terrainLayerIndex == -1) { // Adding to the finalTerrainLayers if this is indeed a newly created or loaded TerrainLayer // (i.e. isn't already part of the TerrainLayers for this Terrain). // Save this layer's index for later on if we make a copy. terrainLayerIndex = finalTerrainLayers.Count; finalTerrainLayers.Add(terrainLayer); // Positive index for alpha map from heightfield (starting at 1) alphaMapIndices.Add(m + 1); } else { // Positive index for alpha map from heightfield (starting at 1) alphaMapIndices[terrainLayerIndex] = m + 1; } // For existing TerrainLayer, make a copy of it if it has custom layer attributes // because we don't want to change the original TerrainLayer. if (!bNewTerrainLayer && layer._hasLayerAttributes) { string bakedTerrainPath = houdiniAsset.GetValidAssetCacheFolderPath(); bakedTerrainPath = HEU_Platform.BuildPath(bakedTerrainPath, relativeFolderPath); TerrainLayer prevTerrainLayer = terrainLayer; terrainLayer = HEU_AssetDatabase.CopyAndLoadAssetAtAnyPath(terrainLayer, bakedTerrainPath, typeof(TerrainLayer), true) as TerrainLayer; if (terrainLayer != null) { // Update the TerrainLayer reference in the list with this copy finalTerrainLayers[terrainLayerIndex] = terrainLayer; } else { Debug.LogErrorFormat("Unable to copy TerrainLayer '{0}' for generating Terrain. " + "Using original TerrainLayer. Will not be able to set any TerrainLayer properties.", layer._layerName); terrainLayer = prevTerrainLayer; bSetTerrainLayerProperties = false; // Again, continuing on to keep proper indexing. } } // Now override layer properties if they have been set via attributes if (bSetTerrainLayerProperties) { LoadLayerPropertiesFromAttributes(session, geoID, partID, terrainLayer, bNewTerrainLayer, defaultTexture); } if (bNewTerrainLayer) { // In order to retain the new TerrainLayer, it must be saved to the AssetDatabase. Object savedObject = null; string layerFileNameWithExt = terrainLayer.name; if (!layerFileNameWithExt.EndsWith(HEU_Defines.HEU_EXT_TERRAINLAYER)) { layerFileNameWithExt += HEU_Defines.HEU_EXT_TERRAINLAYER; } houdiniAsset.AddToAssetDBCache(layerFileNameWithExt, terrainLayer, relativeFolderPath, ref savedObject); } // Store reference layer._terrainLayer = terrainLayer; } // Get existing alpha maps so we can reuse the values if needed float[,,] existingAlphaMaps = terrainData.GetAlphamaps(0, 0, terrainData.alphamapWidth, terrainData.alphamapHeight); terrainData.terrainLayers = finalTerrainLayers.ToArray(); int numTotalAlphaMaps = finalTerrainLayers.Count; #else // Create or update the SplatPrototype based on heightfield layers. // Need to create or reuse SplatPrototype for each layer in heightfield, representing the textures. SplatPrototype[] existingSplats = terrainData.splatPrototypes; // A full rebuild clears out existing splats, but a regular cook keeps them. List<SplatPrototype> finalSplats = new List<SplatPrototype>(existingSplats); // This holds the alpha map indices for each layer that will be added to the TerrainData // The alpha maps could be a mix of existing and new values, so need to know which to use List<int> alphaMapIndices = new List<int>(); // Initially set to use existing alpha maps, then override later on if specified via HF layers. for (int a = 0; a < existingSplats.Length; ++a) { // Negative indices for existing alpha map (offset by -1) alphaMapIndices.Add(-a - 1); } bool bNewSplat = false; HEU_VolumeLayer layer = null; SplatPrototype splatPrototype = null; for (int m = 0; m < numVolumeLayers; ++m) { bNewSplat = false; layer = volumeLayersToProcess[m]; geoID = _ownerNode.GeoID; partID = layer._part.PartID; // Try to find existing SplatPrototype for reuse. But not for full rebuild. splatPrototype = null; if (layer._splatPrototypeIndex >= 0 && layer._splatPrototypeIndex < existingSplats.Length) { splatPrototype = existingSplats[layer._splatPrototypeIndex]; // Positive index for alpha map from heightfield (starting at 1) alphaMapIndices[layer._splatPrototypeIndex] = m + 1; } if (splatPrototype == null) { splatPrototype = new SplatPrototype(); layer._splatPrototypeIndex = finalSplats.Count; finalSplats.Add(splatPrototype); // Positive index for alpha map from heightfield (starting at 1) alphaMapIndices.Add(m + 1); } // Now override splat properties if they have been set via attributes LoadLayerPropertiesFromAttributes(session, geoID, partID, splatPrototype, bNewSplat, defaultTexture); } // On regular cook, get existing alpha maps so we can reuse the values if needed. float[,,] existingAlphaMaps = terrainData.GetAlphamaps(0, 0, terrainData.alphamapWidth, terrainData.alphamapHeight); terrainData.splatPrototypes = finalSplats.ToArray(); int numTotalAlphaMaps = finalSplats.Count; #endif // Set alpha maps by combining with existing alpha maps, and appending new heightfields float[,,] alphamap = null; if (numTotalAlphaMaps > 0 && volumeLayersToProcess.Count > 0) { // Convert the heightfields into alpha maps with layer strengths float[] strengths = new float[volumeLayersToProcess.Count]; for (int m = 0; m < volumeLayersToProcess.Count; ++m) { strengths[m] = volumeLayersToProcess[m]._strength; } alphamap = HEU_TerrainUtility.AppendConvertedHeightFieldToAlphaMap( volumeLayersToProcess[0]._xLength, volumeLayersToProcess[0]._yLength, existingAlphaMaps, heightFields, strengths, alphaMapIndices); // Update the alphamap resolution to the actual size of the first // heightfield layer used for the alphamaps. // Setting the size before setting the alphamas applies proper scaling. int alphamapResolution = volumeLayersToProcess[0]._xLength; terrainData.alphamapResolution = alphamapResolution; terrainData.SetAlphamaps(0, 0, alphamap); } // Tree instances for scattering HEU_TerrainUtility.ApplyScatter(terrainData, _scatterTrees); // If the layers were writen out, this saves the asset DB. Otherwise user has to save it themselves. // Not 100% sure this is needed, but without this the editor doesn't know the terrain asset has been updated // and therefore doesn't import and show the terrain layer. HEU_AssetDatabase.SaveAssetDatabase(); }
public bool GenerateTerrainBuffers(HEU_SessionBase session, HAPI_NodeId nodeID, List<HAPI_PartInfo> volumeParts, List<HAPI_PartInfo> scatterInstancerParts, out List<HEU_LoadBufferVolume> volumeBuffers) { volumeBuffers = null; if (volumeParts.Count == 0) { return true; } volumeBuffers = new List<HEU_LoadBufferVolume>(); int numParts = volumeParts.Count; for (int i = 0; i < numParts; ++i) { HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); bool bResult = session.GetVolumeInfo(nodeID, volumeParts[i].id, ref volumeInfo); if (!bResult || volumeInfo.tupleSize != 1 || volumeInfo.zLength != 1 || volumeInfo.storage != HAPI_StorageType.HAPI_STORAGETYPE_FLOAT) { SetLog(HEU_LoadData.LoadStatus.ERROR, "This heightfield is not supported. Please check documentation."); return false; } if (volumeInfo.xLength != volumeInfo.yLength) { SetLog(HEU_LoadData.LoadStatus.ERROR, "Non-square sized terrain not supported."); return false; } string volumeName = HEU_SessionManager.GetString(volumeInfo.nameSH, session); bool bHeightPart = volumeName.Equals(HEU_Defines.HAPI_HEIGHTFIELD_LAYERNAME_HEIGHT); bool bMaskPart = volumeName.Equals(HEU_Defines.HAPI_HEIGHTFIELD_LAYERNAME_MASK); //Debug.LogFormat("Part name: {0}, GeoName: {1}, Volume Name: {2}, Display: {3}", part.PartName, geoNode.GeoName, volumeName, geoNode.Displayable); // Ignoring mask layer because it is Houdini-specific (same behaviour as regular HDA terrain generation) if (bMaskPart) { continue; } HEU_LoadBufferVolumeLayer layer = new HEU_LoadBufferVolumeLayer(); layer._layerName = volumeName; layer._partID = volumeParts[i].id; layer._heightMapWidth = volumeInfo.xLength; layer._heightMapHeight = volumeInfo.yLength; Matrix4x4 volumeTransformMatrix = HEU_HAPIUtility.GetMatrixFromHAPITransform(ref volumeInfo.transform, false); layer._position = HEU_HAPIUtility.GetPosition(ref volumeTransformMatrix); Vector3 scale = HEU_HAPIUtility.GetScale(ref volumeTransformMatrix); // Calculate real terrain size in both Houdini and Unity. // The height values will be mapped over this terrain size. float gridSpacingX = scale.x * 2f; float gridSpacingY = scale.y * 2f; layer._terrainSizeX = Mathf.Round((volumeInfo.xLength - 1) * gridSpacingX); layer._terrainSizeY = Mathf.Round((volumeInfo.yLength - 1) * gridSpacingY); // Get volume bounds for calculating position offset session.GetVolumeBounds(nodeID, volumeParts[i].id, out layer._minBounds.x, out layer._minBounds.y, out layer._minBounds.z, out layer._maxBounds.x, out layer._maxBounds.y, out layer._maxBounds.z, out layer._center.x, out layer._center.y, out layer._center.z); // Look up TerrainLayer file via attribute if user has set it layer._layerPath = HEU_GeneralUtility.GetAttributeStringValueSingle(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_TERRAINLAYER_FILE_ATTR, HAPI_AttributeOwner.HAPI_ATTROWNER_PRIM); LoadStringFromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_TEXTURE_DIFFUSE_ATTR, ref layer._diffuseTexturePath); LoadStringFromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_TEXTURE_MASK_ATTR, ref layer._maskTexturePath); LoadStringFromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_TEXTURE_NORMAL_ATTR, ref layer._normalTexturePath); LoadFloatFromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_NORMAL_SCALE_ATTR, ref layer._normalScale); LoadFloatFromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_METALLIC_ATTR, ref layer._metallic); LoadFloatFromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_SMOOTHNESS_ATTR, ref layer._smoothness); LoadLayerColorFromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_SPECULAR_ATTR, ref layer._specularColor); LoadLayerVector2FromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_TILE_OFFSET_ATTR, ref layer._tileOffset); LoadLayerVector2FromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_TILE_SIZE_ATTR, ref layer._tileSize); // Get the height values from Houdini along with the min and max height range. layer._normalizedHeights = HEU_TerrainUtility.GetNormalizedHeightmapFromPartWithMinMax(_session, nodeID, volumeParts[i].id, volumeInfo.xLength, volumeInfo.yLength, ref layer._minHeight, ref layer._maxHeight, ref layer._heightRange); // Get the tile index, if it exists, for this part HAPI_AttributeInfo tileAttrInfo = new HAPI_AttributeInfo(); int[] tileAttrData = new int[0]; HEU_GeneralUtility.GetAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.HAPI_HEIGHTFIELD_TILE_ATTR, ref tileAttrInfo, ref tileAttrData, session.GetAttributeIntData); int tileIndex = 0; if (tileAttrInfo.exists && tileAttrData.Length == 1) { tileIndex = tileAttrData[0]; } // Add layer based on tile index if (tileIndex >= 0) { HEU_LoadBufferVolume volumeBuffer = null; for(int j = 0; j < volumeBuffers.Count; ++j) { if (volumeBuffers[j]._tileIndex == tileIndex) { volumeBuffer = volumeBuffers[j]; break; } } if (volumeBuffer == null) { volumeBuffer = new HEU_LoadBufferVolume(); volumeBuffer.InitializeBuffer(volumeParts[i].id, volumeName, false, false); volumeBuffer._tileIndex = tileIndex; volumeBuffers.Add(volumeBuffer); } if (bHeightPart) { // Height layer always first layer volumeBuffer._layers.Insert(0, layer); volumeBuffer._heightMapWidth = layer._heightMapWidth; volumeBuffer._heightMapHeight = layer._heightMapHeight; volumeBuffer._terrainSizeX = layer._terrainSizeX; volumeBuffer._terrainSizeY = layer._terrainSizeY; volumeBuffer._heightRange = (layer._maxHeight - layer._minHeight); // Look up TerrainData file path via attribute if user has set it volumeBuffer._terrainDataPath = HEU_GeneralUtility.GetAttributeStringValueSingle(session, nodeID, volumeBuffer._id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_TERRAINDATA_FILE_ATTR, HAPI_AttributeOwner.HAPI_ATTROWNER_PRIM); // Load the TreePrototype buffers List<HEU_TreePrototypeInfo> treePrototypeInfos = HEU_TerrainUtility.GetTreePrototypeInfosFromPart(session, nodeID, volumeBuffer._id); if (treePrototypeInfos != null) { if (volumeBuffer._scatterTrees == null) { volumeBuffer._scatterTrees = new HEU_VolumeScatterTrees(); } volumeBuffer._scatterTrees._treePrototypInfos = treePrototypeInfos; } } else { volumeBuffer._layers.Add(layer); } } Sleep(); } // Each volume buffer is a self contained terrain tile foreach(HEU_LoadBufferVolume volumeBuffer in volumeBuffers) { List<HEU_LoadBufferVolumeLayer> layers = volumeBuffer._layers; //Debug.LogFormat("Heightfield: tile={0}, layers={1}", tile._tileIndex, layers.Count); int heightMapWidth = volumeBuffer._heightMapWidth; int heightMapHeight = volumeBuffer._heightMapHeight; int numLayers = layers.Count; if (numLayers > 0) { // Convert heightmap values from Houdini to Unity volumeBuffer._heightMap = HEU_TerrainUtility.ConvertHeightMapHoudiniToUnity(heightMapWidth, heightMapHeight, layers[0]._normalizedHeights); Sleep(); // Convert splatmap values from Houdini to Unity. // Start at 2nd index since height is strictly for height values (not splatmap). List<float[]> heightFields = new List<float[]>(); for(int m = 1; m < numLayers; ++m) { heightFields.Add(layers[m]._normalizedHeights); } // The number of maps are the number of splatmaps (ie. non height/mask layers) int numMaps = heightFields.Count; if (numMaps > 0) { // Using the first splatmap size for all splatmaps volumeBuffer._splatMaps = HEU_TerrainUtility.ConvertHeightFieldToAlphaMap(layers[1]._heightMapWidth, layers[1]._heightMapHeight, heightFields); } else { volumeBuffer._splatMaps = null; } volumeBuffer._position = new Vector3((volumeBuffer._terrainSizeX + volumeBuffer._layers[0]._minBounds.x), volumeBuffer._layers[0]._minHeight + volumeBuffer._layers[0]._position.y, volumeBuffer._layers[0]._minBounds.z); } } // Process the scatter instancer parts to get the scatter data for (int i = 0; i < scatterInstancerParts.Count; ++i) { // Find the terrain tile (use primitive attr). Assume 0 tile if not set (i.e. not split into tiles) int terrainTile = 0; HAPI_AttributeInfo tileAttrInfo = new HAPI_AttributeInfo(); int[] tileAttrData = new int[0]; if (HEU_GeneralUtility.GetAttribute(session, nodeID, scatterInstancerParts[i].id, HEU_Defines.HAPI_HEIGHTFIELD_TILE_ATTR, ref tileAttrInfo, ref tileAttrData, session.GetAttributeIntData)) { if (tileAttrData != null && tileAttrData.Length > 0) { terrainTile = tileAttrData[0]; } } // Find the volume layer associated with this part using the terrain tile index HEU_LoadBufferVolume volumeBuffer = GetLoadBufferVolumeFromTileIndex(terrainTile, volumeBuffers); if (volumeBuffer == null) { continue; } HEU_TerrainUtility.PopulateScatterInfo(session, nodeID, scatterInstancerParts[i].id, scatterInstancerParts[i].pointCount, ref volumeBuffer._scatterTrees); } return true; }
/// <summary> /// Creates terrain from given volumeInfo for the given gameObject. /// If gameObject has a valid Terrain component, then it is reused. /// Similarly, if the Terrain component has a valid TerrainData, or if the given terrainData is valid, then it is used. /// Otherwise a new TerrainData is created and set to the Terrain. /// Populates the volumePositionOffset with the heightfield offset position. /// Returns true if successfully created the terrain, otherwise false. /// </summary> /// <param name="session">Houdini Engine session to query heightfield data from</param> /// <param name="volumeInfo">Volume info pertaining to the heightfield to generate the Terrain from</param> /// <param name="geoID">The geometry ID</param> /// <param name="partID">The part ID (height layer)</param> /// <param name="gameObject">The target GameObject containing the Terrain component</param> /// <param name="terrainData">A valid TerrainData to use, or if empty, a new one is created and populated</param> /// <param name="volumePositionOffset">Heightfield offset</param> /// <returns>True if successfully popupated the terrain</returns> public static bool GenerateTerrainFromVolume(HEU_SessionBase session, ref HAPI_VolumeInfo volumeInfo, HAPI_NodeId geoID, HAPI_PartId partID, GameObject gameObject, ref TerrainData terrainData, out Vector3 volumePositionOffset, ref Terrain terrain) { volumePositionOffset = Vector3.zero; if (volumeInfo.zLength == 1 && volumeInfo.tupleSize == 1) { // Heightfields will be converted to terrain in Unity. // Unity requires terrainData.heightmapResolution to be square power of two plus 1 (eg. 513, 257, 129, 65). // Houdini gives volumeInfo.xLength and volumeInfo.yLength which are the number of height values per dimension. // Note that volumeInfo.xLength and volumeInfo.yLength is equal to Houdini heightfield size / grid spacing. // The heightfield grid spacing is given as volumeTransformMatrix.scale but divided by 2 (grid spacing / 2 = volumeTransformMatrix.scale). // It is recommended to use grid spacing of 2. // Use the volumeInfo.transform to get the actual heightfield position and size. Matrix4x4 volumeTransformMatrix = HEU_HAPIUtility.GetMatrixFromHAPITransform(ref volumeInfo.transform, false); Vector3 position = HEU_HAPIUtility.GetPosition(ref volumeTransformMatrix); Vector3 scale = HEU_HAPIUtility.GetScale(ref volumeTransformMatrix); // Calculate real terrain size in both Houdini and Unity. // The height values will be mapped over this terrain size. float gridSpacingX = scale.x * 2f; float gridSpacingY = scale.y * 2f; float terrainSizeX = Mathf.Round((volumeInfo.xLength - 1) * gridSpacingX); float terrainSizeY = Mathf.Round((volumeInfo.yLength - 1) * gridSpacingY); // Test size //float terrainSizeX = Mathf.Round((volumeInfo.xLength) * gridSpacingX); //float terrainSizeY = Mathf.Round((volumeInfo.yLength) * gridSpacingY); //Debug.LogFormat("GS = {0},{1},{2}. SX = {1}. SY = {2}", gridSpacingX, gridSpacingY, terrainSizeX, terrainSizeY); //Debug.LogFormat("HeightField Pos:{0}, Scale:{1}", position, scale.ToString("{0.00}")); //Debug.LogFormat("HeightField tileSize:{0}, xLength:{1}, yLength:{2}", volumeInfo.tileSize.ToString("{0.00}"), volumeInfo.xLength.ToString("{0.00}"), volumeInfo.yLength.ToString("{0.00}")); //Debug.LogFormat("HeightField Terrain Size x:{0}, y:{1}", terrainSizeX.ToString("{0.00}"), terrainSizeY.ToString("{0.00}")); //Debug.LogFormat("HeightField minX={0}, minY={1}, minZ={2}", volumeInfo.minX.ToString("{0.00}"), volumeInfo.minY.ToString("{0.00}"), volumeInfo.minZ.ToString("{0.00}")); const int UNITY_MINIMUM_HEIGHTMAP_RESOLUTION = 33; if (terrainSizeX < UNITY_MINIMUM_HEIGHTMAP_RESOLUTION || terrainSizeY < UNITY_MINIMUM_HEIGHTMAP_RESOLUTION) { Debug.LogWarningFormat("Unity Terrain has a minimum heightmap resolution of {0}. This HDA heightmap size is {1}x{2}." + "\nPlease resize the terrain to a value higher than this.", UNITY_MINIMUM_HEIGHTMAP_RESOLUTION, terrainSizeX, terrainSizeY); return false; } bool bNewTerrain = false; bool bNewTerrainData = false; terrain = gameObject.GetComponent<Terrain>(); if (terrain == null) { terrain = gameObject.AddComponent<Terrain>(); bNewTerrain = true; } #if !HEU_TERRAIN_COLLIDER_DISABLED TerrainCollider collider = HEU_GeneralUtility.GetOrCreateComponent<TerrainCollider>(gameObject); #endif // Look up terrain material, if specified, on the height layer string specifiedTerrainMaterialName = HEU_GeneralUtility.GetMaterialAttributeValueFromPart(session, geoID, partID); // This ensures to reuse existing terraindata, and only creates new if none exist or none provided if (terrain.terrainData == null) { if (terrainData == null) { terrainData = new TerrainData(); bNewTerrainData = true; } terrain.terrainData = terrainData; SetTerrainMaterial(terrain, specifiedTerrainMaterialName); } terrainData = terrain.terrainData; #if !HEU_TERRAIN_COLLIDER_DISABLED collider.terrainData = terrainData; #endif if (bNewTerrain) { #if UNITY_2018_3_OR_NEWER terrain.allowAutoConnect = true; // This has to be set after setting material terrain.drawInstanced = true; #endif } // Heightmap resolution must be square power-of-two plus 1. // Unity will automatically resize terrainData.heightmapResolution so need to handle the changed size (if Unity changed it). int heightMapResolution = volumeInfo.xLength; terrainData.heightmapResolution = heightMapResolution; int terrainResizedDelta = terrainData.heightmapResolution - heightMapResolution; if (terrainResizedDelta < 0) { Debug.LogWarningFormat("Note that Unity automatically resized terrain resolution to {0} from {1}. Use terrain size of power of two plus 1, and grid spacing of 2.", heightMapResolution, terrainData.heightmapResolution); heightMapResolution = terrainData.heightmapResolution; } else if (terrainResizedDelta > 0) { Debug.LogErrorFormat("Unsupported terrain size. Use terrain size of power of two plus 1, and grid spacing of 2. Given size is {0} but Unity resized it to {1}.", heightMapResolution, terrainData.heightmapResolution); return false; } int mapWidth = volumeInfo.xLength; int mapHeight = volumeInfo.yLength; // Get the converted height values from Houdini and find the min and max height range. float minHeight = 0; float maxHeight = 0; float heightRange = 0; bool bUseHeightRangeOverride = true; float[] normalizedHeights = GetNormalizedHeightmapFromPartWithMinMax(session, geoID, partID, volumeInfo.xLength, volumeInfo.yLength, ref minHeight, ref maxHeight, ref heightRange, bUseHeightRangeOverride); float[,] unityHeights = ConvertHeightMapHoudiniToUnity(heightMapResolution, heightMapResolution, normalizedHeights); // The terrainData.baseMapResolution is not set here, but rather left to whatever default Unity uses // The terrainData.alphamapResolution is set later when setting the alphamaps. if (bNewTerrainData) { // 32 is the default for resolutionPerPatch const int detailResolution = 1024; const int resolutionPerPatch = 32; terrainData.SetDetailResolution(detailResolution, resolutionPerPatch); } // Note SetHeights must be called before setting size in next line, as otherwise // the internal terrain size will not change after setting the size. terrainData.SetHeights(0, 0, unityHeights); // Note that Unity uses a default height range of 600 when a flat terrain is created. // Without a non-zero value for the height range, user isn't able to draw heights. // Therefore, set 600 as the value if height range is currently 0 (due to flat heightfield). if (heightRange == 0) { heightRange = terrainData.size.y > 1 ? terrainData.size.y : 600; } terrainData.size = new Vector3(terrainSizeX, heightRange, terrainSizeY); terrain.Flush(); // Unity Terrain has origin at bottom left, whereas Houdini uses centre of terrain. // Use volume bounds to set position offset when using split tiles float xmin, xmax, zmin, zmax, ymin, ymax, xcenter, ycenter, zcenter; session.GetVolumeBounds(geoID, partID, out xmin, out ymin, out zmin, out xmax, out ymax, out zmax, out xcenter, out ycenter, out zcenter); //Debug.LogFormat("xmin: {0}, xmax: {1}, ymin: {2}, ymax: {3}, zmin: {4}, zmax: {5}, xc: {6}, yc: {7}, zc: {8}", // xmin, xmax, ymin, ymax, zmin, zmax, xcenter, ycenter, zcenter); // Offset position is based on size of heightfield float offsetX = (float)heightMapResolution / (float)mapWidth; float offsetZ = (float)heightMapResolution / (float)mapHeight; //Debug.LogFormat("offsetX: {0}, offsetZ: {1}", offsetX, offsetZ); // Use y position from attribute if user has set it float ypos = position.y + minHeight; float userYPos; if (HEU_GeneralUtility.GetAttributeFloatSingle(session, geoID, partID, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_YPOS, out userYPos)) { ypos = userYPos; } // TODO: revisit how the position is calculated volumePositionOffset = new Vector3((terrainSizeX + xmin) * offsetX, ypos, zmin * offsetZ); // Test position //volumePositionOffset = new Vector3(xcenter + mapWidth, ycenter, zcenter - mapHeight); return true; } else { Debug.LogWarning("Non-heightfield volume type not supported!"); } return false; }
/// <summary> /// Upload the alphamaps (TerrainLayers) into heightfield network. /// </summary> /// <param name="session"></param> /// <param name="idt"></param> /// <param name="baseVolumeInfo">The valid base height HAPI_VolumeInfo</param> /// <param name="bMaskSet">This is set to true if a mask layer was uploaded</param> /// <returns>True if successfully uploaded all layers</returns> public bool UploadAlphaMaps(HEU_SessionBase session, HEU_InputDataTerrain idt, ref HAPI_VolumeInfo baseVolumeInfo, out bool bMaskSet) { bool bResult = true; bMaskSet = false; int alphaLayers = idt._terrainData.alphamapLayers; if (alphaLayers < 1) { return(bResult); } int sizeX = idt._terrainData.alphamapWidth; int sizeY = idt._terrainData.alphamapHeight; int totalSize = sizeX * sizeY; float[,,] alphaMaps = idt._terrainData.GetAlphamaps(0, 0, sizeX, sizeY); float[][] alphaMapsConverted = new float[alphaLayers][]; // Convert the alphamap layers to double arrays. for (int m = 0; m < alphaLayers; ++m) { alphaMapsConverted[m] = new float[totalSize]; for (int j = 0; j < sizeY; j++) { for (int i = 0; i < sizeX; i++) { // Flip for coordinate system change float h = alphaMaps[i, (sizeY - j - 1), m]; alphaMapsConverted[m][i + j * sizeX] = h; } } } // Create volume layers for all alpha maps and upload values. bool bMaskLayer = false; int inputLayerIndex = 1; for (int m = 0; m < alphaLayers; ++m) { #if UNITY_2018_3_OR_NEWER string layerName = idt._terrainData.terrainLayers[m].name; #else string layerName = "unity_alphamap_" + m + 1; #endif // The Unity layer name could contain '.terrainlayer' and spaces. Remove them because Houdini doesn't allow // spaces, and the extension isn't necessary. layerName = layerName.Replace(" ", "_"); int extIndex = layerName.LastIndexOf(HEU_Defines.HEU_EXT_TERRAINLAYER); if (extIndex > 0) { layerName = layerName.Remove(extIndex); } //Debug.Log("Processing terrain layer: " + layerName); HAPI_NodeId alphaLayerID = HEU_Defines.HEU_INVALID_NODE_ID; if (layerName.Equals(HEU_Defines.HAPI_HEIGHTFIELD_LAYERNAME_HEIGHT)) { // Skip height (base) layer (since it has been uploaded already) continue; } else if (layerName.Equals(HEU_Defines.HAPI_HEIGHTFIELD_LAYERNAME_MASK)) { //Debug.Log("Mask layer found! Skipping creating the HF."); bMaskSet = true; bMaskLayer = true; alphaLayerID = idt._maskNodeID; } else { bMaskLayer = false; if (!session.CreateHeightfieldInputVolumeNode(idt._heightfieldNodeID, out alphaLayerID, layerName, Mathf.RoundToInt(sizeX * idt._voxelSize), Mathf.RoundToInt(sizeY * idt._voxelSize), idt._voxelSize)) { bResult = false; Debug.LogError("Failed to create input volume node for layer " + layerName); break; } } //Debug.Log("Uploading terrain layer: " + layerName); if (!SetHeightFieldData(session, alphaLayerID, 0, alphaMapsConverted[m], layerName, ref baseVolumeInfo)) { bResult = false; break; } #if UNITY_2018_3_OR_NEWER SetTerrainLayerAttributesToHeightField(session, alphaLayerID, 0, idt._terrainData.terrainLayers[m]); #endif if (!session.CommitGeo(alphaLayerID)) { bResult = false; Debug.LogError("Failed to commit volume layer " + layerName); break; } if (!bMaskLayer) { // Connect to the merge node but starting from index 1 since index 0 is height layer if (!session.ConnectNodeInput(idt._mergeNodeID, inputLayerIndex + 1, alphaLayerID, 0)) { bResult = false; Debug.LogError("Unable to connect new volume node for layer " + layerName); break; } inputLayerIndex++; } } return(bResult); }
public void UpdateLayerFromPart(HEU_SessionBase session, HEU_PartData part) { HEU_GeoNode geoNode = part.ParentGeoNode; HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); bool bResult = session.GetVolumeInfo(geoNode.GeoID, part.PartID, ref volumeInfo); if (!bResult || volumeInfo.tupleSize != 1 || volumeInfo.zLength != 1 || volumeInfo.storage != HAPI_StorageType.HAPI_STORAGETYPE_FLOAT) { return; } string volumeName = HEU_SessionManager.GetString(volumeInfo.nameSH, session); part.SetVolumeLayerName(volumeName); //Debug.LogFormat("Part name: {0}, GeoName: {1}, Volume Name: {2}, Display: {3}", part.PartName, geoNode.GeoName, volumeName, geoNode.Displayable); HFLayerType layerType = HEU_TerrainUtility.GetHeightfieldLayerType(session, geoNode.GeoID, part.PartID, volumeName); HEU_VolumeLayer layer = GetLayer(volumeName); if (layer == null) { layer = new HEU_VolumeLayer(); layer._layerName = volumeName; if (layerType == HFLayerType.HEIGHT) { _layers.Insert(0, layer); } else if (layerType != HFLayerType.MASK) { _layers.Add(layer); } } layer._part = part; layer._xLength = volumeInfo.xLength; layer._yLength = volumeInfo.yLength; layer._layerType = layerType; if (layerType != HFLayerType.MASK) { GetPartLayerAttributes(session, geoNode.GeoID, part.PartID, layer); } if (layerType != HFLayerType.HEIGHT) { // Non-height parts don't have any outputs as they are simply layers carrying info part.DestroyAllData(); } else { // Height part // Might contain terrain properties via attributes (i.e. not layer specific, but for entire terrain) // Scatter Tree Prototypes List<HEU_TreePrototypeInfo> treePrototypeInfos = HEU_TerrainUtility.GetTreePrototypeInfosFromPart(session, geoNode.GeoID, part.PartID); if (treePrototypeInfos != null) { if (_scatterTrees == null) { _scatterTrees = new HEU_VolumeScatterTrees(); } _scatterTrees._treePrototypInfos = treePrototypeInfos; } HEU_TerrainUtility.PopulateDetailProperties(session, geoNode.GeoID, part.PartID, ref _detailProperties); } if (!_updatedLayers.Contains(layer)) { if (layerType == HFLayerType.HEIGHT) { _updatedLayers.Insert(0, layer); } else if (layerType != HFLayerType.MASK) { _updatedLayers.Add(layer); } } }
public bool GenerateTerrainBuffers(HEU_SessionBase session, HAPI_NodeId nodeID, List<HAPI_PartInfo> volumeParts, out List<HEU_LoadBufferVolume> volumeBuffers) { volumeBuffers = null; if (volumeParts.Count == 0) { return true; } volumeBuffers = new List<HEU_LoadBufferVolume>(); int numParts = volumeParts.Count; for (int i = 0; i < numParts; ++i) { HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); bool bResult = session.GetVolumeInfo(nodeID, volumeParts[i].id, ref volumeInfo); if (!bResult || volumeInfo.tupleSize != 1 || volumeInfo.zLength != 1 || volumeInfo.storage != HAPI_StorageType.HAPI_STORAGETYPE_FLOAT) { SetLog(HEU_LoadData.LoadStatus.ERROR, "This heightfield is not supported. Please check documentation."); return false; } if (volumeInfo.xLength != volumeInfo.yLength) { SetLog(HEU_LoadData.LoadStatus.ERROR, "Non-square sized terrain not supported."); return false; } string volumeName = HEU_SessionManager.GetString(volumeInfo.nameSH, session); bool bHeightPart = volumeName.Equals("height"); //Debug.LogFormat("Part name: {0}, GeoName: {1}, Volume Name: {2}, Display: {3}", part.PartName, geoNode.GeoName, volumeName, geoNode.Displayable); HEU_LoadBufferVolumeLayer layer = new HEU_LoadBufferVolumeLayer(); layer._layerName = volumeName; layer._partID = volumeParts[i].id; layer._heightMapSize = volumeInfo.xLength; Matrix4x4 volumeTransformMatrix = HEU_HAPIUtility.GetMatrixFromHAPITransform(ref volumeInfo.transform, false); layer._position = HEU_HAPIUtility.GetPosition(ref volumeTransformMatrix); Vector3 scale = HEU_HAPIUtility.GetScale(ref volumeTransformMatrix); // Calculate real terrain size in both Houdini and Unity. // The height values will be mapped over this terrain size. float gridSpacingX = scale.x * 2f; float gridSpacingY = scale.y * 2f; layer._terrainSizeX = Mathf.Round((volumeInfo.xLength - 1) * gridSpacingX); layer._terrainSizeY = Mathf.Round((volumeInfo.yLength - 1) * gridSpacingY); // Get volume bounds for calculating position offset session.GetVolumeBounds(nodeID, volumeParts[i].id, out layer._minBounds.x, out layer._minBounds.y, out layer._minBounds.z, out layer._maxBounds.x, out layer._maxBounds.y, out layer._maxBounds.z, out layer._center.x, out layer._center.y, out layer._center.z); LoadStringFromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_TEXTURE_DIFFUSE_ATTR, ref layer._diffuseTexturePath); LoadStringFromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_TEXTURE_MASK_ATTR, ref layer._maskTexturePath); LoadStringFromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_TEXTURE_NORMAL_ATTR, ref layer._normalTexturePath); LoadFloatFromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_NORMAL_SCALE_ATTR, ref layer._normalScale); LoadFloatFromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_METALLIC_ATTR, ref layer._metallic); LoadFloatFromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_SMOOTHNESS_ATTR, ref layer._smoothness); LoadLayerColorFromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_SPECULAR_ATTR, ref layer._specularColor); LoadLayerVector2FromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_TILE_OFFSET_ATTR, ref layer._tileOffset); LoadLayerVector2FromAttribute(session, nodeID, volumeParts[i].id, HEU_Defines.DEFAULT_UNITY_HEIGHTFIELD_TILE_SIZE_ATTR, ref layer._tileSize); // Get the height values from Houdini and find the min and max height range. if (!HEU_GeometryUtility.GetHeightfieldValues(session, volumeInfo.xLength, volumeInfo.yLength, nodeID, volumeParts[i].id, ref layer._rawHeights, ref layer._minHeight, ref layer._maxHeight)) { return false; } // TODO: Tried to replace above with this, but it flattens the heights //layer._rawHeights = HEU_GeometryUtility.GetHeightfieldFromPart(_session, nodeID, volumeParts[i].id, "part", volumeInfo.xLength); // Get the tile index, if it exists, for this part HAPI_AttributeInfo tileAttrInfo = new HAPI_AttributeInfo(); int[] tileAttrData = new int[0]; HEU_GeneralUtility.GetAttribute(session, nodeID, volumeParts[i].id, "tile", ref tileAttrInfo, ref tileAttrData, session.GetAttributeIntData); int tileIndex = 0; if (tileAttrInfo.exists && tileAttrData.Length == 1) { tileIndex = tileAttrData[0]; } // Add layer based on tile index if (tileIndex >= 0) { HEU_LoadBufferVolume volumeBuffer = null; for(int j = 0; j < volumeBuffers.Count; ++j) { if (volumeBuffers[j]._tileIndex == tileIndex) { volumeBuffer = volumeBuffers[j]; break; } } if (volumeBuffer == null) { volumeBuffer = new HEU_LoadBufferVolume(); volumeBuffer.InitializeBuffer(volumeParts[i].id, volumeName, false, false); volumeBuffer._tileIndex = tileIndex; volumeBuffers.Add(volumeBuffer); } if (bHeightPart) { // Height layer always first layer volumeBuffer._layers.Insert(0, layer); volumeBuffer._heightMapSize = layer._heightMapSize; volumeBuffer._terrainSizeX = layer._terrainSizeX; volumeBuffer._terrainSizeY = layer._terrainSizeY; volumeBuffer._heightRange = (layer._maxHeight - layer._minHeight); } else { volumeBuffer._layers.Add(layer); } } Sleep(); } // Each volume buffer is a self contained terrain tile foreach(HEU_LoadBufferVolume volumeBuffer in volumeBuffers) { List<HEU_LoadBufferVolumeLayer> layers = volumeBuffer._layers; //Debug.LogFormat("Heightfield: tile={0}, layers={1}", tile._tileIndex, layers.Count); int heightMapSize = volumeBuffer._heightMapSize; int numLayers = layers.Count; if (numLayers > 0) { // Convert heightmap values from Houdini to Unity volumeBuffer._heightMap = HEU_GeometryUtility.ConvertHeightMapHoudiniToUnity(heightMapSize, layers[0]._rawHeights, layers[0]._minHeight, layers[0]._maxHeight); Sleep(); // Convert splatmap values from Houdini to Unity. List<float[]> heightFields = new List<float[]>(); for(int m = 1; m < numLayers; ++m) { heightFields.Add(layers[m]._rawHeights); } volumeBuffer._splatMaps = HEU_GeometryUtility.ConvertHeightSplatMapHoudiniToUnity(heightMapSize, heightFields); volumeBuffer._position = new Vector3((volumeBuffer._terrainSizeX + volumeBuffer._layers[0]._minBounds.x), volumeBuffer._layers[0]._minHeight + volumeBuffer._layers[0]._position.y, volumeBuffer._layers[0]._minBounds.z); } } return true; }
public void UpdateLayerFromPart(HEU_SessionBase session, HEU_PartData part) { HEU_GeoNode geoNode = part.ParentGeoNode; HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); bool bResult = session.GetVolumeInfo(geoNode.GeoID, part.PartID, ref volumeInfo); if (!bResult || volumeInfo.tupleSize != 1 || volumeInfo.zLength != 1 || volumeInfo.storage != HAPI_StorageType.HAPI_STORAGETYPE_FLOAT) { return; } string volumeName = HEU_SessionManager.GetString(volumeInfo.nameSH, session); part.SetVolumeLayerName(volumeName); //Debug.LogFormat("Part name: {0}, GeoName: {1}, Volume Name: {2}, Display: {3}", part.PartName, geoNode.GeoName, volumeName, geoNode.Displayable); HEU_VolumeLayer.HFLayerType layerType = GetHeightfieldLayerType(session, geoNode.GeoID, part.PartID, volumeName); HEU_VolumeLayer layer = GetLayer(volumeName); if (layer == null) { layer = new HEU_VolumeLayer(); layer._layerName = volumeName; if (layerType == HEU_VolumeLayer.HFLayerType.HEIGHT) { _layers.Insert(0, layer); } else if(layerType != HEU_VolumeLayer.HFLayerType.MASK) { _layers.Add(layer); } } layer._part = part; layer._xLength = volumeInfo.xLength; layer._yLength = volumeInfo.yLength; layer._layerType = layerType; if (layerType != HEU_VolumeLayer.HFLayerType.MASK) { GetPartLayerAttributes(session, geoNode.GeoID, part.PartID, layer); } if (layerType != HEU_VolumeLayer.HFLayerType.HEIGHT) { // Non-height parts don't have any outputs as they are simply layers carrying info part.DestroyAllData(); } else { // Height part // Might contain terrain properties via attributes (i.e. not layer specific, but for entire terrain) // Scatter Tree Prototypes List<HEU_TreePrototypeInfo> treePrototypeInfos = HEU_TerrainUtility.GetTreePrototypeInfosFromPart(session, geoNode.GeoID, part.PartID); if (treePrototypeInfos != null) { if (_scatterTrees == null) { _scatterTrees = new HEU_VolumeScatterTrees(); } _scatterTrees._treePrototypInfos = treePrototypeInfos; } // Detail distance HAPI_AttributeInfo detailDistanceAttrInfo = new HAPI_AttributeInfo(); int[] detailDistances = new int[0]; HEU_GeneralUtility.GetAttribute(session, geoNode.GeoID, part.PartID, HEU_Defines.HEIGHTFIELD_DETAIL_DISTANCE, ref detailDistanceAttrInfo, ref detailDistances, session.GetAttributeIntData); // Scatter Detail Resolution Per Patch (note that Detail Resolution comes from HF layer size) HAPI_AttributeInfo resolutionPatchAttrInfo = new HAPI_AttributeInfo(); int[] resolutionPatches = new int[0]; HEU_GeneralUtility.GetAttribute(session, geoNode.GeoID, part.PartID, HEU_Defines.HEIGHTFIELD_DETAIL_RESOLUTION_PER_PATCH, ref resolutionPatchAttrInfo, ref resolutionPatches, session.GetAttributeIntData); if (_detailProperties == null) { _detailProperties = new HEU_DetailProperties(); } // Unity only supports 1 set of detail resolution properties per terrain int arraySize = 1; if (detailDistanceAttrInfo.exists && detailDistances.Length >= arraySize) { _detailProperties._detailDistance = detailDistances[0]; } if (resolutionPatchAttrInfo.exists && resolutionPatches.Length >= arraySize) { _detailProperties._detailResolutionPerPatch = resolutionPatches[0]; } } if (!_updatedLayers.Contains(layer)) { if (layerType == HEU_VolumeLayer.HFLayerType.HEIGHT) { _updatedLayers.Insert(0, layer); } else if (layerType != HEU_VolumeLayer.HFLayerType.MASK) { _updatedLayers.Add(layer); } } }
public virtual bool SetVolumeInfo(HAPI_NodeId nodeID, HAPI_PartId partID, ref HAPI_VolumeInfo volumeInfo) { return false; }
/// <summary> /// Creates a heightfield network inside the same object as connectNodeID. /// Uploads the terrain data from inputObject into the new heightfield network, incuding /// all terrain layers/alphamaps. /// </summary> /// <param name="session">Session that connectNodeID exists in</param> /// <param name="connectNodeID">The node to connect the network to. Most likely a SOP/merge node</param> /// <param name="inputObject">The gameobject containing the Terrain components</param> /// <param name="inputNodeID">The created heightfield network node ID</param> /// <returns>True if created network and uploaded heightfield data.</returns> public override bool CreateInputNodeWithDataUpload(HEU_SessionBase session, HAPI_NodeId connectNodeID, GameObject inputObject, out HAPI_NodeId inputNodeID) { inputNodeID = HEU_Defines.HEU_INVALID_NODE_ID; // Create input node, cook it, then upload the geometry data if (!HEU_HAPIUtility.IsNodeValidInHoudini(session, connectNodeID)) { Debug.LogError("Connection node is invalid."); return(false); } HEU_InputDataTerrain idt = GenerateTerrainDataFromGameObject(inputObject); if (idt == null) { return(false); } HAPI_NodeId parentNodeID = HEU_HAPIUtility.GetParentNodeID(session, connectNodeID); idt._parentNodeID = parentNodeID; if (!CreateHeightFieldInputNode(session, idt)) { return(false); } HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); if (!UploadHeightValuesWithTransform(session, idt, ref volumeInfo)) { return(false); } inputNodeID = idt._heightfieldNodeID; bool bMaskSet = false; if (!UploadAlphaMaps(session, idt, ref volumeInfo, out bMaskSet)) { return(false); } if (!bMaskSet) { // While the default HF created by the input node also creates a default mask layer, // we still need to set the mask layer's transform. So this uses the base VolumeInfo // to do just that. if (!SetMaskLayer(session, idt, ref volumeInfo)) { return(false); } } if (!session.CookNode(inputNodeID, false)) { Debug.LogError("New input node failed to cook!"); return(false); } return(true); }
public void UpdateLayerFromPart(HEU_SessionBase session, HEU_PartData part) { HEU_GeoNode geoNode = part.ParentGeoNode; HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); bool bResult = session.GetVolumeInfo(geoNode.GeoID, part.PartID, ref volumeInfo); if (!bResult || volumeInfo.tupleSize != 1 || volumeInfo.zLength != 1 || volumeInfo.storage != HAPI_StorageType.HAPI_STORAGETYPE_FLOAT) { return; } string volumeName = HEU_SessionManager.GetString(volumeInfo.nameSH, session); part.SetVolumeLayerName(volumeName); //Debug.LogFormat("Part name: {0}, GeoName: {1}, Volume Name: {2}, Display: {3}", part.PartName, geoNode.GeoName, volumeName, geoNode.Displayable); bool bHeightPart = volumeName.Equals(HEU_Defines.HAPI_HEIGHTFIELD_LAYERNAME_HEIGHT); bool bMaskPart = volumeName.Equals(HEU_Defines.HAPI_HEIGHTFIELD_LAYERNAME_MASK); HEU_VolumeLayer layer = GetLayer(volumeName); if (layer == null) { layer = new HEU_VolumeLayer(); layer._layerName = volumeName; if (bHeightPart) { _layers.Insert(0, layer); } else if(!bMaskPart) { _layers.Add(layer); } } layer._part = part; layer._xLength = volumeInfo.xLength; layer._yLength = volumeInfo.yLength; if (!bMaskPart) { GetPartLayerAttributes(session, geoNode.GeoID, part.PartID, layer); } if (!bHeightPart) { // Non-height parts don't have any outputs as they are simply layers carrying info part.DestroyAllData(); } else { // Height part List<HEU_TreePrototypeInfo> treePrototypeInfos = HEU_TerrainUtility.GetTreePrototypeInfosFromPart(session, geoNode.GeoID, part.PartID); if (treePrototypeInfos != null) { if (_scatterTrees == null) { _scatterTrees = new HEU_VolumeScatterTrees(); } _scatterTrees._treePrototypInfos = treePrototypeInfos; } } if (!_updatedLayers.Contains(layer)) { if (bHeightPart) { _updatedLayers.Insert(0, layer); } else if (!bMaskPart) { _updatedLayers.Add(layer); } } }
public void GenerateTerrainWithAlphamaps(HEU_SessionBase session, HEU_HoudiniAsset houdiniAsset) { if(_layers == null || _layers.Count == 0) { Debug.LogError("Unable to generate terrain due to lack of heightfield layers!"); return; } HEU_VolumeLayer baseLayer = _layers[0]; HAPI_VolumeInfo baseVolumeInfo = new HAPI_VolumeInfo(); bool bResult = session.GetVolumeInfo(_ownerNode.GeoID, baseLayer._part.PartID, ref baseVolumeInfo); if (!bResult) { Debug.LogErrorFormat("Unable to get volume info for layer {0}!", baseLayer._layerName); return; } TerrainData terrainData = null; Vector3 terrainOffsetPosition = Vector3.zero; // Generate the terrain and terrain data from the heightmap's height layer bResult = HEU_GeometryUtility.GenerateTerrainFromVolume(session, ref baseVolumeInfo, baseLayer._part.ParentGeoNode.GeoID, baseLayer._part.PartID, baseLayer._part.OutputGameObject, out terrainData, out terrainOffsetPosition); if (!bResult) { return; } baseLayer._part.SetTerrainData(terrainData); baseLayer._part.SetTerrainOffsetPosition(terrainOffsetPosition); int terrainSize = terrainData.heightmapResolution; // Now set the alphamaps (textures with masks) for the other layers // First, preprocess all layers to get heightfield arrays, converted to proper size // Then, merge into a float[x,y,map] List<float[]> heightFields = new List<float[]>(); List<HEU_VolumeLayer> validLayers = new List<HEU_VolumeLayer>(); int numLayers = _layers.Count; for(int i = 1; i < numLayers; ++i) { float[] hf = HEU_GeometryUtility.GetHeightfieldFromPart(session, _ownerNode.GeoID, _layers[i]._part.PartID, _layers[i]._part.PartName, terrainSize); if (hf != null && hf.Length > 0) { heightFields.Add(hf); validLayers.Add(_layers[i]); } } // Total maps is masks plus base height layer int numMaps = heightFields.Count + 1; // Assign floats to alpha map float[,,] alphamap = new float[terrainSize, terrainSize, numMaps]; for (int y = 0; y < terrainSize; ++y) { for (int x = 0; x < terrainSize; ++x) { float f = 0f; for (int m = numMaps - 1; m > 0; --m) { float a = heightFields[m - 1][y + terrainSize * x]; a = Mathf.Clamp01(a - f) * validLayers[m - 1]._strength; alphamap[x, y, m] = a; f += a; } // Base layer gets leftover value alphamap[x, y, 0] = Mathf.Clamp01(1.0f - f) * baseLayer._strength; } } #if UNITY_2018_3_OR_NEWER // Create TerrainLayer for each heightfield layer // Note that at time of this implementation the new Unity terrain // is still in beta. Therefore, the following layer creation is subject // to change. TerrainLayer[] terrainLayers = new TerrainLayer[numMaps]; for (int m = 0; m < numMaps; ++m) { terrainLayers[m] = new TerrainLayer(); HEU_VolumeLayer layer = (m == 0) ? baseLayer : validLayers[m - 1]; terrainLayers[m].diffuseTexture = layer._diffuseTexture; terrainLayers[m].diffuseRemapMin = Vector4.zero; terrainLayers[m].diffuseRemapMax = Vector4.one; terrainLayers[m].maskMapTexture = layer._maskTexture; terrainLayers[m].maskMapRemapMin = Vector4.zero; terrainLayers[m].maskMapRemapMax = Vector4.one; terrainLayers[m].metallic = layer._metallic; terrainLayers[m].normalMapTexture = layer._normalTexture; terrainLayers[m].normalScale = layer._normalScale; terrainLayers[m].smoothness = layer._smoothness; terrainLayers[m].specular = layer._specularColor; terrainLayers[m].tileOffset = layer._tileOffset; if (layer._tileSize.magnitude == 0f) { // Use texture size if tile size is 0 layer._tileSize = new Vector2(layer._diffuseTexture.width, layer._diffuseTexture.height); } terrainLayers[m].tileSize = layer._tileSize; } terrainData.terrainLayers = terrainLayers; #else // Need to create SplatPrototype for each layer in heightfield, representing the textures. SplatPrototype[] splatPrototypes = new SplatPrototype[numMaps]; for (int m = 0; m < numMaps; ++m) { splatPrototypes[m] = new SplatPrototype(); HEU_VolumeLayer layer = (m == 0) ? baseLayer : validLayers[m - 1]; splatPrototypes[m].texture = layer._diffuseTexture; splatPrototypes[m].tileOffset = layer._tileOffset; if(layer._tileSize.magnitude == 0f) { // Use texture size if tile size is 0 layer._tileSize = new Vector2(layer._diffuseTexture.width, layer._diffuseTexture.height); } splatPrototypes[m].tileSize = layer._tileSize; splatPrototypes[m].metallic = layer._metallic; splatPrototypes[m].smoothness = layer._smoothness; splatPrototypes[m].normalMap = layer._normalTexture; } terrainData.splatPrototypes = splatPrototypes; #endif terrainData.SetAlphamaps(0, 0, alphamap); }
/// <summary> /// Process the part at the given index, creating its data (geometry), /// and adding it to the list of parts. /// </summary> /// <param name="session"></param> /// <param name="partID"></param> /// <returns>A valid HEU_PartData if it has been successfully processed.</returns> private void ProcessPart(HEU_SessionBase session, int partID, ref HAPI_PartInfo partInfo, ref HEU_PartData partData) { HEU_HoudiniAsset parentAsset = ParentAsset; bool bResult = true; //Debug.LogFormat("Part: name={0}, id={1}, type={2}, instanced={3}, instance count={4}, instance part count={5}", HEU_SessionManager.GetString(partInfo.nameSH, session), partID, partInfo.type, partInfo.isInstanced, partInfo.instanceCount, partInfo.instancedPartCount); #if HEU_PROFILER_ON float processPartStartTime = Time.realtimeSinceStartup; #endif bool isPartEditable = IsIntermediateOrEditable(); bool isAttribInstancer = false; if (IsGeoInputType()) { // Setup for input node to accept inputs if (_inputNode == null) { string partName = HEU_SessionManager.GetString(partInfo.nameSH, session); _inputNode = HEU_InputNode.CreateSetupInput(GeoID, 0, partName, HEU_InputNode.InputNodeType.NODE, ParentAsset); if (_inputNode != null) { ParentAsset.AddInputNode(_inputNode); } } if (HEU_HAPIUtility.IsSupportedPolygonType(partInfo.type) && partInfo.vertexCount == 0) { // No geometry for input asset if (partData != null) { // Clean up existing part HEU_PartData.DestroyPart(partData); partData = null; } // No need to process further since we don't have geometry return; } } else { // Preliminary check for attribute instancing (mesh type with no verts but has points with instances) if (HEU_HAPIUtility.IsSupportedPolygonType(partInfo.type) && partInfo.vertexCount == 0 && partInfo.pointCount > 0) { HAPI_AttributeInfo instanceAttrInfo = new HAPI_AttributeInfo(); HEU_GeneralUtility.GetAttributeInfo(session, GeoID, partID, HEU_PluginSettings.UnityInstanceAttr, ref instanceAttrInfo); if (instanceAttrInfo.exists && instanceAttrInfo.count > 0) { isAttribInstancer = true; } } } if (partInfo.type == HAPI_PartType.HAPI_PARTTYPE_INVALID) { // Clean up invalid parts if (partData != null) { HEU_PartData.DestroyPart(partData); partData = null; } } else if (partInfo.type < HAPI_PartType.HAPI_PARTTYPE_MAX) { // Process the part based on type. Keep or ignore. // We treat parts of type curve as curves, along with geo nodes that are editable and type curves if (partInfo.type == HAPI_PartType.HAPI_PARTTYPE_CURVE) { if (partData == null) { partData = ScriptableObject.CreateInstance <HEU_PartData>(); } partData.Initialize(session, partID, GeoID, _containerObjectNode.ObjectID, this, ref partInfo, HEU_PartData.PartOutputType.CURVE, isPartEditable, _containerObjectNode.IsInstancer(), false); SetupGameObjectAndTransform(partData, parentAsset); partData.ProcessCurvePart(session); } else if (partInfo.type == HAPI_PartType.HAPI_PARTTYPE_VOLUME) { // We only process "height" volume parts. Other volume parts are ignored for now. #if TERRAIN_SUPPORTED HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); bResult = session.GetVolumeInfo(GeoID, partID, ref volumeInfo); if (!bResult) { Debug.LogErrorFormat("Unable to get volume info for geo node {0} and part {1} ", GeoID, partID); } else { if (Displayable && !IsIntermediateOrEditable()) { if (partData == null) { partData = ScriptableObject.CreateInstance <HEU_PartData>(); } else { // Clear volume data (case where switching from polygonal mesh to volume output) partData.ClearGeneratedMeshOutput(); } partData.Initialize(session, partID, GeoID, _containerObjectNode.ObjectID, this, ref partInfo, HEU_PartData.PartOutputType.VOLUME, isPartEditable, _containerObjectNode.IsInstancer(), false); SetupGameObjectAndTransform(partData, ParentAsset); } } #else Debug.LogWarningFormat("Terrain (heightfield volume) is not yet supported."); #endif } else if (partInfo.type == HAPI_PartType.HAPI_PARTTYPE_INSTANCER || isAttribInstancer) { if (partData == null) { partData = ScriptableObject.CreateInstance <HEU_PartData>(); } else { partData.ClearGeneratedMeshOutput(); partData.ClearGeneratedVolumeOutput(); } partData.Initialize(session, partID, GeoID, _containerObjectNode.ObjectID, this, ref partInfo, HEU_PartData.PartOutputType.INSTANCER, isPartEditable, _containerObjectNode.IsInstancer(), isAttribInstancer); SetupGameObjectAndTransform(partData, parentAsset); } else if (HEU_HAPIUtility.IsSupportedPolygonType(partInfo.type)) { if (partData == null) { partData = ScriptableObject.CreateInstance <HEU_PartData>(); } else { // Clear volume data (case where switching from something other output to mesh) partData.ClearGeneratedVolumeOutput(); } partData.Initialize(session, partID, GeoID, _containerObjectNode.ObjectID, this, ref partInfo, HEU_PartData.PartOutputType.MESH, isPartEditable, _containerObjectNode.IsInstancer(), false); // This check allows to ignore editable non-display nodes by default, but commented out to allow // them for now. Users can also ignore them by turning on IgnoreNonDisplayNodes //if (Displayable || (Editable && ParentAsset.EditableNodesToolsEnabled)) { SetupGameObjectAndTransform(partData, parentAsset); } } else { Debug.LogWarningFormat("Unsupported part type {0}", partInfo.type); } if (partData != null) { // Success! _parts.Add(partData); // Set unique name for the part string partName = HEU_PluginSettings.UseFullPathNamesForOutput ? GeneratePartFullName(partData.PartName) : partData.PartName; partData.SetGameObjectName(partName); // For intermediate or default-type editable nodes, setup the HEU_AttributeStore if (isPartEditable) { partData.SyncAttributesStore(session, _geoInfo.nodeId, ref partInfo); } else { // Remove attributes store if it has it partData.DestroyAttributesStore(); } } } #if HEU_PROFILER_ON Debug.LogFormat("PART PROCESS TIME:: NAME={0}, TIME={1}", HEU_SessionManager.GetString(partInfo.nameSH, session), (Time.realtimeSinceStartup - processPartStartTime)); #endif }
public static bool GenerateTerrainFromVolume(HEU_SessionBase session, ref HAPI_VolumeInfo volumeInfo, HAPI_NodeId geoID, HAPI_PartId partID, GameObject gameObject, out TerrainData terrainData, out Vector3 volumePositionOffset) { terrainData = null; volumePositionOffset = Vector3.zero; if (volumeInfo.zLength == 1 && volumeInfo.tupleSize == 1) { // Heightfields will be converted to terrain in Unity. // Unity requires terrainData.heightmapResolution to be square power of two plus 1 (eg. 513, 257, 129, 65). // Houdini gives volumeInfo.xLength and volumeInfo.yLength which are the number of height values per dimension. // Note that volumeInfo.xLength and volumeInfo.yLength is equal to Houdini heightfield size / grid spacing. // The heightfield grid spacing is given as volumeTransformMatrix.scale but divided by 2 (grid spacing / 2 = volumeTransformMatrix.scale). // Number of heightfield values int totalHeightValues = volumeInfo.xLength * volumeInfo.yLength; // Use the volumeInfo.transform to get the actual heightfield position and size. Matrix4x4 volumeTransformMatrix = HEU_HAPIUtility.GetMatrixFromHAPITransform(ref volumeInfo.transform, false); Vector3 position = HEU_HAPIUtility.GetPosition(ref volumeTransformMatrix); Vector3 scale = HEU_HAPIUtility.GetScale(ref volumeTransformMatrix); // The scaled size is the real world size of the terrain in both Houdini and Unity. // Unity terrain uses this to scale up the height values we set. int scaledSizeX = Mathf.RoundToInt(volumeInfo.xLength * scale.x) * 2; int scaledSizeZ = Mathf.RoundToInt(volumeInfo.yLength * scale.z) * 2; //Debug.LogFormat("HeightField Pos:{0}, Scale:{1}", position, scale); //Debug.LogFormat("HeightField tileSize:{0}, xLength:{1}, yLength:{2}", volumeInfo.tileSize, volumeInfo.xLength, volumeInfo.yLength); //Debug.LogFormat("HeightField Scaled Size x:{0}, y:{1}", scaledSizeX, scaledSizeZ); //Debug.LogFormat("HeightField minX={0}, minY={1}, minZ={2}", volumeInfo.minX, volumeInfo.minY, volumeInfo.minZ); const int UNITY_MINIMUM_HEIGHTMAP_RESOLUTION = 33; if (scaledSizeX < UNITY_MINIMUM_HEIGHTMAP_RESOLUTION || scaledSizeZ < UNITY_MINIMUM_HEIGHTMAP_RESOLUTION) { Debug.LogWarningFormat("Unity Terrain has a minimum heightmap resolution of {0}. This HDA heightmap size is {1}x{2}." + "\nPlease resize the terrain to a value higher than this.", UNITY_MINIMUM_HEIGHTMAP_RESOLUTION, scaledSizeX, scaledSizeZ); return false; } int mapWidth = volumeInfo.xLength; int mapHeight = volumeInfo.yLength; // Unity terrain requires (though not strictly) a square size that is power of two plus one, // so we calculate that from the given volumeInfo.xLength and volumeInfo.yLength. int squareSizePlusOne = mapWidth >= mapHeight ? mapWidth : mapHeight; if (!Mathf.IsPowerOfTwo(squareSizePlusOne - 1)) { squareSizePlusOne = Mathf.NextPowerOfTwo(squareSizePlusOne - 1) + 1; } //Debug.LogFormat("MAPHEIGHT: {0}, SQUARE: {1}", mapHeight, squareSize); // If our valid square size is greater than half than the scaled true size, then the mapping will need // extra padding since we need to go up to the next power of two plus one. if ((squareSizePlusOne * 2) > scaledSizeX || (squareSizePlusOne * 2) > scaledSizeZ) { Debug.LogWarningFormat("The specified heightfield size will require extra padding when converting heightfield to terrain due to Houdini to Unity size differences!" + "\nRecommended to use heightfield size that is power of two plus two (eg. 514, 258, 130, 66)."); scaledSizeX = Mathf.RoundToInt(squareSizePlusOne * scale.x) * 2; scaledSizeZ = Mathf.RoundToInt(squareSizePlusOne * scale.z) * 2; } // Get the height values from Houdini and find the min and max height range. float[] heightValues = new float[totalHeightValues]; bool bResult = session.GetHeightFieldData(geoID, partID, heightValues, 0, totalHeightValues); if (!bResult) { return false; } float minHeight = heightValues[0]; float maxHeight = minHeight; for (int i = 0; i < totalHeightValues; ++i) { float f = heightValues[i]; if (f > maxHeight) { maxHeight = f; } else if (f < minHeight) { minHeight = f; } } const int UNITY_MAX_HEIGHT_RANGE = 65536; float heightRange = (maxHeight - minHeight); if (Mathf.RoundToInt(heightRange) > UNITY_MAX_HEIGHT_RANGE) { Debug.LogWarningFormat("Unity Terrain has maximum height range of {0}. This HDA height range is {1}, so it will be maxed out at {0}.\nPlease resize to within valid range!", UNITY_MAX_HEIGHT_RANGE, Mathf.RoundToInt(heightRange)); heightRange = UNITY_MAX_HEIGHT_RANGE; } int paddingWidth = squareSizePlusOne - mapWidth; int paddingLeft = Mathf.CeilToInt(paddingWidth * 0.5f); int paddingRight = squareSizePlusOne - paddingLeft; //Debug.LogFormat("Padding: Width={0}, Left={1}, Right={2}", paddingWidth, paddingLeft, paddingRight); int paddingHeight = squareSizePlusOne - mapHeight; int paddingTop = Mathf.CeilToInt(paddingHeight * 0.5f); int paddingBottom = squareSizePlusOne - paddingTop; //Debug.LogFormat("Padding: Height={0}, Top={1}, Bottom={2}", paddingHeight, paddingTop, paddingBottom); // Set height values at centre of the terrain, with padding on the sides if we resized float[,] unityHeights = new float[squareSizePlusOne, squareSizePlusOne]; for (int y = 0; y < squareSizePlusOne; ++y) { for (int x = 0; x < squareSizePlusOne; ++x) { if (y >= paddingTop && y < (paddingBottom) && x >= paddingLeft && x < (paddingRight)) { int ay = x - paddingLeft; int ax = y - paddingTop; // Unity expects normalized height values float h = heightValues[ay + ax * mapWidth] - minHeight; float f = h / heightRange; // Flip for right-hand to left-handed coordinate system int ix = x; int iy = squareSizePlusOne - (y + 1); // Unity expects height array indexing to be [y, x]. unityHeights[ix, iy] = f; } } } terrainData = new TerrainData(); // Heightmap resolution must be square power-of-two plus 1 terrainData.heightmapResolution = squareSizePlusOne; // TODO: Pull these from plugin settings perhaps? terrainData.baseMapResolution = 1024; terrainData.alphamapResolution = 1024; int detailResolution = squareSizePlusOne - 1; // 128 is the maximum for resolutionPerPatch const int resolutionPerPatch = 128; terrainData.SetDetailResolution(detailResolution, resolutionPerPatch); // Note SetHeights must be called before setting size in next line, as otherwise // the internal terrain size will not change after setting the size. terrainData.SetHeights(0, 0, unityHeights); terrainData.size = new Vector3(scaledSizeX, heightRange, scaledSizeZ); Terrain terrain = HEU_GeneralUtility.GetOrCreateComponent<Terrain>(gameObject); TerrainCollider collider = HEU_GeneralUtility.GetOrCreateComponent<TerrainCollider>(gameObject); terrain.terrainData = terrainData; collider.terrainData = terrainData; terrain.Flush(); // Unity Terrain has origin at bottom left, whereas // Houdini uses centre of terrain. We'll need to move the terrain half way back in X and half way up in Z // TODO: revisit to properly get volume position offset when splitting tiles float xmin, xmax, zmin, zmax, ymin, ymax, xcenter, ycenter, zcenter; session.GetVolumeBounds(geoID, partID, out xmin, out ymin, out zmin, out xmax, out ymax, out zmax, out xcenter, out ycenter, out zcenter); //Debug.LogFormat("xmin: {0}, xmax: {1}, ymin: {2}, ymax: {3}, zmin: {4}, zmax: {5}, xc: {6}, yc: {7}, zc: {8}", // xmin, xmax, ymin, ymax, zmin, zmax, xcenter, ycenter, zcenter); // Offset position is based on size of heightfield float offsetX = (float)squareSizePlusOne / (float)mapWidth; float offsetZ = (float)squareSizePlusOne / (float)mapHeight; volumePositionOffset = new Vector3(-position.x * offsetX, minHeight + position.y, position.z * offsetZ); return true; } else { Debug.LogWarning("Non-heightfield volume type not supported!"); } return false; }
/// <summary> /// Retrieves the height values from Houdini for the given volume part. /// </summary> public static float[] GetHeightfieldFromPart(HEU_SessionBase session, HAPI_NodeId geoID, HAPI_PartId partID, string partName, int terrainSize) { HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); bool bResult = session.GetVolumeInfo(geoID, partID, ref volumeInfo); if (!bResult) { return null; } int volumeXLength = volumeInfo.xLength; int volumeYLength = volumeInfo.yLength; // Number of heightfield values int totalHeightValues = volumeXLength * volumeYLength; float minHeight = float.MaxValue; float maxHeight = float.MinValue; float[] heightValues = new float[totalHeightValues]; if (!GetHeightfieldValues(session, volumeXLength, volumeYLength, geoID, partID, ref heightValues, ref minHeight, ref maxHeight)) { return null; } float heightRange = (maxHeight - minHeight); if (heightRange == 0f) { heightRange = 1f; } //Debug.LogFormat("{0} : {1}", HEU_SessionManager.GetString(volumeInfo.nameSH, session), heightRange); // Remap height values to fit terrain size int paddingWidth = terrainSize - volumeXLength; int paddingLeft = Mathf.CeilToInt(paddingWidth * 0.5f); int paddingRight = terrainSize - paddingLeft; //Debug.LogFormat("Padding: Width={0}, Left={1}, Right={2}", paddingWidth, paddingLeft, paddingRight); int paddingHeight = terrainSize - volumeYLength; int paddingTop = Mathf.CeilToInt(paddingHeight * 0.5f); int paddingBottom = terrainSize - paddingTop; //Debug.LogFormat("Padding: Height={0}, Top={1}, Bottom={2}", paddingHeight, paddingTop, paddingBottom); // Set height values at centre of the terrain, with padding on the sides if we resized float[] resizedHeightValues = new float[terrainSize * terrainSize]; for (int y = 0; y < terrainSize; ++y) { for (int x = 0; x < terrainSize; ++x) { if (y >= paddingTop && y < (paddingBottom) && x >= paddingLeft && x < (paddingRight)) { int ay = x - paddingLeft; int ax = y - paddingTop; float f = heightValues[ay + ax * volumeXLength] - minHeight; f /= heightRange; // Flip for right-hand to left-handed coordinate system int ix = x; int iy = terrainSize - (y + 1); // Unity expects height array indexing to be [y, x]. resizedHeightValues[iy + ix * terrainSize] = f; } } } return resizedHeightValues; }
/// <summary> /// Process the part at the given index, creating its data (geometry), /// and adding it to the list of parts. /// </summary> /// <param name="session"></param> /// <param name="partID"></param> /// <returns>A valid HEU_PartData if it has been successfully processed.</returns> private void ProcessPart(HEU_SessionBase session, int partID, ref HAPI_PartInfo partInfo, ref HEU_PartData partData) { HEU_HoudiniAsset parentAsset = ParentAsset; bool bResult = true; //Debug.LogFormat("Part: name={0}, id={1}, type={2}, instanced={3}, instance count={4}, instance part count={5}", HEU_SessionManager.GetString(partInfo.nameSH, session), partID, partInfo.type, partInfo.isInstanced, partInfo.instanceCount, partInfo.instancedPartCount); #if HEU_PROFILER_ON float processPartStartTime = Time.realtimeSinceStartup; #endif bool isPartEditable = IsIntermediateOrEditable(); if (IsGeoInputType()) { // Setup for input node to accept inputs if (_inputNode == null) { string partName = HEU_SessionManager.GetString(partInfo.nameSH, session); _inputNode = HEU_InputNode.CreateSetupInput(GeoID, 0, partName, HEU_InputNode.InputNodeType.NODE, ParentAsset); if (_inputNode != null) { ParentAsset.AddInputNode(_inputNode); } } if (partInfo.type == HAPI_PartType.HAPI_PARTTYPE_MESH && partInfo.vertexCount == 0) { // No geometry for input asset if (partData != null) { // Clean up existing part HEU_PartData.DestroyPart(partData); partData = null; } // No need to process further since we don't have geometry return; } } if (partInfo.type == HAPI_PartType.HAPI_PARTTYPE_INVALID) { // Clean up invalid parts if (partData != null) { HEU_PartData.DestroyPart(partData); partData = null; } } else if (partInfo.type < HAPI_PartType.HAPI_PARTTYPE_MAX) { // Process the part based on type. Keep or ignore. // We treat parts of type curve as curves, along with geo nodes that are editable and type curves if (partInfo.type == HAPI_PartType.HAPI_PARTTYPE_CURVE) { if (partData == null) { partData = ScriptableObject.CreateInstance <HEU_PartData>(); } partData.Initialize(session, partID, GeoID, _containerObjectNode.ObjectID, this, ref partInfo, HEU_PartData.PartOutputType.CURVE, isPartEditable); SetupGameObjectAndTransform(partData, parentAsset); partData.ProcessCurvePart(session); } else if (partInfo.type == HAPI_PartType.HAPI_PARTTYPE_VOLUME) { // We only process "height" volume parts. Other volume parts are ignored for now. #if TERRAIN_SUPPORTED HAPI_VolumeInfo volumeInfo = new HAPI_VolumeInfo(); bResult = session.GetVolumeInfo(GeoID, partID, ref volumeInfo); if (!bResult) { Debug.LogErrorFormat("Unable to get volume info for geo node {0} and part {1} ", GeoID, partID); } else { if (Displayable && !IsIntermediateOrEditable()) { if (partData == null) { partData = ScriptableObject.CreateInstance <HEU_PartData>(); } partData.Initialize(session, partID, GeoID, _containerObjectNode.ObjectID, this, ref partInfo, HEU_PartData.PartOutputType.VOLUME, isPartEditable); SetupGameObjectAndTransform(partData, ParentAsset); } } #else Debug.LogWarningFormat("Terrain (heightfield volume) is not yet supported."); #endif } else if (partInfo.type == HAPI_PartType.HAPI_PARTTYPE_MESH) { if (partData == null) { partData = ScriptableObject.CreateInstance <HEU_PartData>(); } partData.Initialize(session, partID, GeoID, _containerObjectNode.ObjectID, this, ref partInfo, HEU_PartData.PartOutputType.MESH, isPartEditable); SetupGameObjectAndTransform(partData, parentAsset); } else if (partInfo.type == HAPI_PartType.HAPI_PARTTYPE_INSTANCER) { if (partData == null) { partData = ScriptableObject.CreateInstance <HEU_PartData>(); } partData.Initialize(session, partID, GeoID, _containerObjectNode.ObjectID, this, ref partInfo, HEU_PartData.PartOutputType.INSTANCER, isPartEditable); SetupGameObjectAndTransform(partData, parentAsset); } else { Debug.LogWarningFormat("Unsupported part type {0}", partInfo.type); } if (partData != null) { // Success! _parts.Add(partData); // Set unique name for the part string partFullName = GeneratePartFullName(partData.PartName); partFullName = HEU_EditorUtility.GetUniqueNameForSibling(ParentAsset.RootGameObject.transform, partFullName); partData.SetGameObjectName(partFullName); // For intermediate or default-type editable nodes, setup the HEU_AttributeStore if (isPartEditable) { partData.SyncAttributesStore(session, _geoInfo.nodeId, ref partInfo); } else { // Remove attributes store if it has it partData.DestroyAttributesStore(); } HEU_GeneralUtility.AssignUnityTag(session, GeoID, partData.PartID, partData._gameObject); HEU_GeneralUtility.MakeStaticIfHasAttribute(session, GeoID, partData.PartID, partData._gameObject); } } #if HEU_PROFILER_ON Debug.LogFormat("PART PROCESS TIME:: NAME={0}, TIME={1}", HEU_SessionManager.GetString(partInfo.nameSH, session), (Time.realtimeSinceStartup - processPartStartTime)); #endif }