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); } } } Terrain terrain = null; // 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, ref terrain); 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[]> normalizedHeightfields = new List<float[]>(); // Corresponding list of HF volume layers to process as splatmaps List<HEU_VolumeLayer> terrainLayersToProcess = new List<HEU_VolumeLayer>(); List<int[,]> convertedDetailMaps = new List<int[,]>(); List<HEU_DetailPrototype> detailPrototypes = new List<HEU_DetailPrototype>(); 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. // The layers are normalized and split into detail and terrain layers. for(int i = 1; i < numLayers; ++i) { if (_layers[i]._layerType == HFLayerType.DETAIL) { if (_detailProperties == null) { _detailProperties = new HEU_DetailProperties(); } // Convert to detail map, and add to list to set later int[,] normalizedDetail = HEU_TerrainUtility.GetDetailMapFromPart( session, _ownerNode.GeoID, _layers[i]._part.PartID, out _detailProperties._detailResolution); if (normalizedDetail != null && normalizedDetail.Length > 0) { convertedDetailMaps.Add(normalizedDetail); detailPrototypes.Add(_layers[i]._detailPrototype); } } else { // Convert to normalized heightfield 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) { normalizedHeightfields.Add(normalizedHF); terrainLayersToProcess.Add(_layers[i]); } } } int numTerrainLayersToProcess = terrainLayersToProcess.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 < numTerrainLayersToProcess; ++m) { bNewTerrainLayer = false; bSetTerrainLayerProperties = true; layer = terrainLayersToProcess[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 < numTerrainLayersToProcess; ++m) { bNewSplat = false; layer = terrainLayersToProcess[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 && terrainLayersToProcess.Count > 0) { // Convert the heightfields into alpha maps with layer strengths float[] strengths = new float[terrainLayersToProcess.Count]; for (int m = 0; m < terrainLayersToProcess.Count; ++m) { strengths[m] = terrainLayersToProcess[m]._strength; } alphamap = HEU_TerrainUtility.AppendConvertedHeightFieldToAlphaMap( terrainLayersToProcess[0]._xLength, terrainLayersToProcess[0]._yLength, existingAlphaMaps, normalizedHeightfields, 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 = terrainLayersToProcess[0]._xLength; terrainData.alphamapResolution = alphamapResolution; terrainData.SetAlphamaps(0, 0, alphamap); } // Scattering - trees and details HEU_TerrainUtility.ApplyScatterTrees(terrainData, _scatterTrees); HEU_TerrainUtility.ApplyDetailLayers(terrain, terrainData, _detailProperties, detailPrototypes, convertedDetailMaps); // 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(); }