private void GenerateTerrain(List<HEU_LoadBufferVolume> terrainBuffers) { Transform parent = this.gameObject.transform; // Directory to store generated terrain files. string outputTerrainpath = GetOutputCacheDirectory(); outputTerrainpath = HEU_Platform.BuildPath(outputTerrainpath, "Terrain"); int numVolumes = terrainBuffers.Count; for(int t = 0; t < numVolumes; ++t) { if (terrainBuffers[t]._heightMap != null) { GameObject newGameObject = new GameObject("heightfield_" + terrainBuffers[t]._tileIndex); Transform newTransform = newGameObject.transform; newTransform.parent = parent; HEU_GeneratedOutput generatedOutput = new HEU_GeneratedOutput(); generatedOutput._outputData._gameObject = newGameObject; Terrain terrain = HEU_GeneralUtility.GetOrCreateComponent<Terrain>(newGameObject); TerrainCollider collider = HEU_GeneralUtility.GetOrCreateComponent<TerrainCollider>(newGameObject); if (!string.IsNullOrEmpty(terrainBuffers[t]._terrainDataPath)) { // Load the source TerrainData, then make a unique copy of it in the cache folder TerrainData sourceTerrainData = HEU_AssetDatabase.LoadAssetAtPath(terrainBuffers[t]._terrainDataPath, typeof(TerrainData)) as TerrainData; if (sourceTerrainData == null) { Debug.LogWarningFormat("TerrainData, set via attribute, not found at: {0}", terrainBuffers[t]._terrainDataPath); } terrain.terrainData = HEU_AssetDatabase.CopyUniqueAndLoadAssetAtAnyPath(sourceTerrainData, outputTerrainpath, typeof(TerrainData)) as TerrainData; if (terrain.terrainData != null) { // Store path so that it can be deleted on clean up AddGeneratedOutputFilePath(HEU_AssetDatabase.GetAssetPath(terrain.terrainData)); } } if (terrain.terrainData == null) { terrain.terrainData = new TerrainData(); } TerrainData terrainData = terrain.terrainData; collider.terrainData = terrainData; HEU_TerrainUtility.SetTerrainMaterial(terrain, terrainBuffers[t]._specifiedTerrainMaterialName); #if UNITY_2018_3_OR_NEWER terrain.allowAutoConnect = true; // This has to be set after setting material terrain.drawInstanced = true; #endif int heightMapSize = terrainBuffers[t]._heightMapWidth; terrainData.heightmapResolution = heightMapSize; if (terrainData.heightmapResolution != heightMapSize) { Debug.LogErrorFormat("Unsupported terrain size: {0}", heightMapSize); continue; } // 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. // 32 is the default for resolutionPerPatch const int detailResolution = 1024; const int resolutionPerPatch = 32; terrainData.SetDetailResolution(detailResolution, resolutionPerPatch); terrainData.SetHeights(0, 0, terrainBuffers[t]._heightMap); // 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). float heightRange = terrainBuffers[t]._heightRange; if (heightRange == 0) { heightRange = 600; } terrainData.size = new Vector3(terrainBuffers[t]._terrainSizeX, heightRange, terrainBuffers[t]._terrainSizeY); terrain.Flush(); // Set position HAPI_Transform hapiTransformVolume = new HAPI_Transform(true); hapiTransformVolume.position[0] += terrainBuffers[t]._position[0]; hapiTransformVolume.position[1] += terrainBuffers[t]._position[1]; hapiTransformVolume.position[2] += terrainBuffers[t]._position[2]; HEU_HAPIUtility.ApplyLocalTransfromFromHoudiniToUnity(ref hapiTransformVolume, newTransform); // Set layers Texture2D defaultTexture = HEU_VolumeCache.LoadDefaultSplatTexture(); int numLayers = terrainBuffers[t]._splatLayers.Count; #if UNITY_2018_3_OR_NEWER // Create TerrainLayer for each heightfield layer. // Note that height and mask layers are ignored (i.e. not created as TerrainLayers). // Since height layer is first, only process layers from 2nd index onwards. if (numLayers > 1) { // 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); for (int m = 1; m < numLayers; ++m) { TerrainLayer terrainlayer = null; int terrainLayerIndex = -1; bool bSetTerrainLayerProperties = true; HEU_LoadBufferVolumeLayer layer = terrainBuffers[t]._splatLayers[m]; // Look up TerrainLayer file via attribute if user has set it if (!string.IsNullOrEmpty(layer._layerPath)) { terrainlayer = HEU_AssetDatabase.LoadAssetAtPath(layer._layerPath, typeof(TerrainLayer)) as TerrainLayer; if (terrainlayer == null) { Debug.LogWarningFormat("TerrainLayer, set via attribute, not found at: {0}", layer._layerPath); continue; } else { // Always check if its part of existing list so as not to add it again terrainLayerIndex = HEU_TerrainUtility.GetTerrainLayerIndex(terrainlayer, existingTerrainLayers); } } if (terrainlayer == null) { terrainlayer = new TerrainLayer(); terrainLayerIndex = finalTerrainLayers.Count; finalTerrainLayers.Add(terrainlayer); } else { // 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 (layer._hasLayerAttributes && terrainLayerIndex >= 0) { // Copy the TerrainLayer file TerrainLayer prevTerrainLayer = terrainlayer; terrainlayer = HEU_AssetDatabase.CopyAndLoadAssetAtAnyPath(terrainlayer, outputTerrainpath, typeof(TerrainLayer), true) as TerrainLayer; if (terrainlayer != null) { // Update the TerrainLayer reference in the list with this copy finalTerrainLayers[terrainLayerIndex] = terrainlayer; // Store path for clean up later AddGeneratedOutputFilePath(HEU_AssetDatabase.GetAssetPath(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. } } } if (bSetTerrainLayerProperties) { if (!string.IsNullOrEmpty(layer._diffuseTexturePath)) { terrainlayer.diffuseTexture = HEU_MaterialFactory.LoadTexture(layer._diffuseTexturePath); } if (terrainlayer.diffuseTexture == null) { terrainlayer.diffuseTexture = defaultTexture; } terrainlayer.diffuseRemapMin = Vector4.zero; terrainlayer.diffuseRemapMax = Vector4.one; if (!string.IsNullOrEmpty(layer._maskTexturePath)) { terrainlayer.maskMapTexture = HEU_MaterialFactory.LoadTexture(layer._maskTexturePath); } terrainlayer.maskMapRemapMin = Vector4.zero; terrainlayer.maskMapRemapMax = Vector4.one; terrainlayer.metallic = layer._metallic; if (!string.IsNullOrEmpty(layer._normalTexturePath)) { terrainlayer.normalMapTexture = HEU_MaterialFactory.LoadTexture(layer._normalTexturePath); } terrainlayer.normalScale = layer._normalScale; terrainlayer.smoothness = layer._smoothness; terrainlayer.specular = layer._specularColor; terrainlayer.tileOffset = layer._tileOffset; if (layer._tileSize.magnitude == 0f && terrainlayer.diffuseTexture != null) { // Use texture size if tile size is 0 layer._tileSize = new Vector2(terrainlayer.diffuseTexture.width, terrainlayer.diffuseTexture.height); } terrainlayer.tileSize = layer._tileSize; } } terrainData.terrainLayers = finalTerrainLayers.ToArray(); } #else // Need to create SplatPrototype for each layer in heightfield, representing the textures. SplatPrototype[] splatPrototypes = new SplatPrototype[numLayers]; for (int m = 0; m < numLayers; ++m) { splatPrototypes[m] = new SplatPrototype(); HEU_LoadBufferVolumeLayer layer = terrainBuffers[t]._splatLayers[m]; Texture2D diffuseTexture = null; if (!string.IsNullOrEmpty(layer._diffuseTexturePath)) { diffuseTexture = HEU_MaterialFactory.LoadTexture(layer._diffuseTexturePath); } if (diffuseTexture == null) { diffuseTexture = defaultTexture; } splatPrototypes[m].texture = diffuseTexture; splatPrototypes[m].tileOffset = layer._tileOffset; if (layer._tileSize.magnitude == 0f && diffuseTexture != null) { // Use texture size if tile size is 0 layer._tileSize = new Vector2(diffuseTexture.width, diffuseTexture.height); } splatPrototypes[m].tileSize = layer._tileSize; splatPrototypes[m].metallic = layer._metallic; splatPrototypes[m].smoothness = layer._smoothness; if (!string.IsNullOrEmpty(layer._normalTexturePath)) { splatPrototypes[m].normalMap = HEU_MaterialFactory.LoadTexture(layer._normalTexturePath); } } terrainData.splatPrototypes = splatPrototypes; #endif // Set the splatmaps if (terrainBuffers[t]._splatMaps != null) { // Set the alphamap size before setting the alphamaps to get correct scaling // The alphamap size comes from the first alphamap layer int alphamapResolution = terrainBuffers[t]._heightMapWidth; if (numLayers > 1) { alphamapResolution = terrainBuffers[t]._splatLayers[1]._heightMapWidth; } terrainData.alphamapResolution = alphamapResolution; terrainData.SetAlphamaps(0, 0, terrainBuffers[t]._splatMaps); } // Set the tree scattering if (terrainBuffers[t]._scatterTrees != null) { HEU_TerrainUtility.ApplyScatterTrees(terrainData, terrainBuffers[t]._scatterTrees); } // Set the detail layers if (terrainBuffers[t]._detailPrototypes != null) { HEU_TerrainUtility.ApplyDetailLayers(terrain, terrainData, terrainBuffers[t]._detailProperties, terrainBuffers[t]._detailPrototypes, terrainBuffers[t]._detailMaps); } terrainBuffers[t]._generatedOutput = generatedOutput; _generatedOutputs.Add(generatedOutput); SetOutputVisiblity(terrainBuffers[t]); } } }
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; }
private void GenerateTerrain(List<HEU_LoadBufferVolume> terrainBuffers) { Transform parent = this.gameObject.transform; int numVolumes = terrainBuffers.Count; for(int t = 0; t < numVolumes; ++t) { if (terrainBuffers[t]._heightMap != null) { GameObject newGameObject = new GameObject("heightfield_" + terrainBuffers[t]._tileIndex); Transform newTransform = newGameObject.transform; newTransform.parent = parent; HEU_GeneratedOutput generatedOutput = new HEU_GeneratedOutput(); generatedOutput._outputData._gameObject = newGameObject; Terrain terrain = HEU_GeneralUtility.GetOrCreateComponent<Terrain>(newGameObject); TerrainCollider collider = HEU_GeneralUtility.GetOrCreateComponent<TerrainCollider>(newGameObject); if (!string.IsNullOrEmpty(terrainBuffers[t]._terrainDataPath)) { terrain.terrainData = HEU_AssetDatabase.LoadAssetAtPath(terrainBuffers[t]._terrainDataPath, typeof(TerrainData)) as TerrainData; if (terrain.terrainData == null) { Debug.LogWarningFormat("TerrainData, set via attribute, not found at: {0}", terrainBuffers[t]._terrainDataPath); } } if (terrain.terrainData == null) { terrain.terrainData = new TerrainData(); } TerrainData terrainData = terrain.terrainData; collider.terrainData = terrainData; HEU_TerrainUtility.SetTerrainMaterial(terrain); #if UNITY_2018_3_OR_NEWER terrain.allowAutoConnect = true; // This has to be set after setting material terrain.drawInstanced = true; #endif int heightMapSize = terrainBuffers[t]._heightMapWidth; terrainData.heightmapResolution = heightMapSize; if (terrainData.heightmapResolution != heightMapSize) { Debug.LogErrorFormat("Unsupported terrain size: {0}", heightMapSize); continue; } // 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. // 32 is the default for resolutionPerPatch const int detailResolution = 1024; const int resolutionPerPatch = 32; terrainData.SetDetailResolution(detailResolution, resolutionPerPatch); terrainData.SetHeights(0, 0, terrainBuffers[t]._heightMap); // 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). float heightRange = terrainBuffers[t]._heightRange; if (heightRange == 0) { heightRange = 600; } terrainData.size = new Vector3(terrainBuffers[t]._terrainSizeX, heightRange, terrainBuffers[t]._terrainSizeY); terrain.Flush(); // Set position HAPI_Transform hapiTransformVolume = new HAPI_Transform(true); hapiTransformVolume.position[0] += terrainBuffers[t]._position[0]; hapiTransformVolume.position[1] += terrainBuffers[t]._position[1]; hapiTransformVolume.position[2] += terrainBuffers[t]._position[2]; HEU_HAPIUtility.ApplyLocalTransfromFromHoudiniToUnity(ref hapiTransformVolume, newTransform); // Set layers Texture2D defaultTexture = HEU_VolumeCache.LoadDefaultSplatTexture(); int numLayers = terrainBuffers[t]._layers.Count; #if UNITY_2018_3_OR_NEWER // Create TerrainLayer for each heightfield layer. // Note that height and mask layers are ignored (i.e. not created as TerrainLayers). // Since height layer is first, only process layers from 2nd index onwards. if (numLayers > 1) { TerrainLayer[] terrainLayers = new TerrainLayer[numLayers - 1]; for (int m = 1; m < numLayers; ++m) { TerrainLayer terrainlayer = null; HEU_LoadBufferVolumeLayer layer = terrainBuffers[t]._layers[m]; // Look up TerrainLayer file via attribute if user has set it if (!string.IsNullOrEmpty(layer._layerPath)) { terrainlayer = HEU_AssetDatabase.LoadAssetAtPath(layer._layerPath, typeof(TerrainLayer)) as TerrainLayer; if (terrainlayer == null) { Debug.LogWarningFormat("TerrainLayer, set via attribute, not found at: {0}", layer._layerPath); continue; } } if (terrainlayer == null) { terrainlayer = new TerrainLayer(); } if (!string.IsNullOrEmpty(layer._diffuseTexturePath)) { terrainlayer.diffuseTexture = HEU_MaterialFactory.LoadTexture(layer._diffuseTexturePath); } if (terrainlayer.diffuseTexture == null) { terrainlayer.diffuseTexture = defaultTexture; } terrainlayer.diffuseRemapMin = Vector4.zero; terrainlayer.diffuseRemapMax = Vector4.one; if (!string.IsNullOrEmpty(layer._maskTexturePath)) { terrainlayer.maskMapTexture = HEU_MaterialFactory.LoadTexture(layer._maskTexturePath); } terrainlayer.maskMapRemapMin = Vector4.zero; terrainlayer.maskMapRemapMax = Vector4.one; terrainlayer.metallic = layer._metallic; if (!string.IsNullOrEmpty(layer._normalTexturePath)) { terrainlayer.normalMapTexture = HEU_MaterialFactory.LoadTexture(layer._normalTexturePath); } terrainlayer.normalScale = layer._normalScale; terrainlayer.smoothness = layer._smoothness; terrainlayer.specular = layer._specularColor; terrainlayer.tileOffset = layer._tileOffset; if (layer._tileSize.magnitude == 0f && terrainlayer.diffuseTexture != null) { // Use texture size if tile size is 0 layer._tileSize = new Vector2(terrainlayer.diffuseTexture.width, terrainlayer.diffuseTexture.height); } terrainlayer.tileSize = layer._tileSize; // Note index is m - 1 due to skipping height layer terrainLayers[m - 1] = terrainlayer; } terrainData.terrainLayers = terrainLayers; } #else // Need to create SplatPrototype for each layer in heightfield, representing the textures. SplatPrototype[] splatPrototypes = new SplatPrototype[numLayers]; for (int m = 0; m < numLayers; ++m) { splatPrototypes[m] = new SplatPrototype(); HEU_LoadBufferVolumeLayer layer = terrainBuffers[t]._layers[m]; Texture2D diffuseTexture = null; if (!string.IsNullOrEmpty(layer._diffuseTexturePath)) { diffuseTexture = HEU_MaterialFactory.LoadTexture(layer._diffuseTexturePath); } if (diffuseTexture == null) { diffuseTexture = defaultTexture; } splatPrototypes[m].texture = diffuseTexture; splatPrototypes[m].tileOffset = layer._tileOffset; if (layer._tileSize.magnitude == 0f && diffuseTexture != null) { // Use texture size if tile size is 0 layer._tileSize = new Vector2(diffuseTexture.width, diffuseTexture.height); } splatPrototypes[m].tileSize = layer._tileSize; splatPrototypes[m].metallic = layer._metallic; splatPrototypes[m].smoothness = layer._smoothness; if (!string.IsNullOrEmpty(layer._normalTexturePath)) { splatPrototypes[m].normalMap = HEU_MaterialFactory.LoadTexture(layer._normalTexturePath); } } terrainData.splatPrototypes = splatPrototypes; #endif // Set the splatmaps if (terrainBuffers[t]._splatMaps != null) { // Set the alphamap size before setting the alphamaps to get correct scaling // The alphamap size comes from the first alphamap layer int alphamapResolution = terrainBuffers[t]._heightMapWidth; if (numLayers > 1) { alphamapResolution = terrainBuffers[t]._layers[1]._heightMapWidth; } terrainData.alphamapResolution = alphamapResolution; terrainData.SetAlphamaps(0, 0, terrainBuffers[t]._splatMaps); } // Set the tree scattering if (terrainBuffers[t]._scatterTrees != null) { HEU_TerrainUtility.ApplyScatter(terrainData, terrainBuffers[t]._scatterTrees); } terrainBuffers[t]._generatedOutput = generatedOutput; _generatedOutputs.Add(generatedOutput); SetOutputVisiblity(terrainBuffers[t]); } } }
/// <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; }
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; }
private void GenerateTerrain(List<HEU_LoadBufferVolume> terrainBuffers) { Transform parent = this.gameObject.transform; int numVolues = terrainBuffers.Count; for(int t = 0; t < numVolues; ++t) { if (terrainBuffers[t]._heightMap != null) { GameObject newGameObject = new GameObject("heightfield_" + terrainBuffers[t]._tileIndex); Transform newTransform = newGameObject.transform; newTransform.parent = parent; HEU_GeneratedOutput generatedOutput = new HEU_GeneratedOutput(); generatedOutput._outputData._gameObject = newGameObject; Terrain terrain = HEU_GeneralUtility.GetOrCreateComponent<Terrain>(newGameObject); TerrainCollider collider = HEU_GeneralUtility.GetOrCreateComponent<TerrainCollider>(newGameObject); terrain.terrainData = new TerrainData(); TerrainData terrainData = terrain.terrainData; collider.terrainData = terrainData; int heightMapSize = terrainBuffers[t]._heightMapSize; terrainData.heightmapResolution = heightMapSize; if (terrainData.heightmapResolution != heightMapSize) { Debug.LogErrorFormat("Unsupported terrain size: {0}", heightMapSize); continue; } terrainData.baseMapResolution = heightMapSize; terrainData.alphamapResolution = heightMapSize; const int resolutionPerPatch = 128; terrainData.SetDetailResolution(resolutionPerPatch, resolutionPerPatch); terrainData.SetHeights(0, 0, terrainBuffers[t]._heightMap); terrainData.size = new Vector3(terrainBuffers[t]._terrainSizeX, terrainBuffers[t]._heightRange, terrainBuffers[t]._terrainSizeY); terrain.Flush(); // Set position HAPI_Transform hapiTransformVolume = new HAPI_Transform(true); hapiTransformVolume.position[0] += terrainBuffers[t]._position[0]; hapiTransformVolume.position[1] += terrainBuffers[t]._position[1]; hapiTransformVolume.position[2] += terrainBuffers[t]._position[2]; HEU_HAPIUtility.ApplyLocalTransfromFromHoudiniToUnity(ref hapiTransformVolume, newTransform); // Set layers Texture2D defaultTexture = HEU_VolumeCache.LoadDefaultSplatTexture(); int numLayers = terrainBuffers[t]._layers.Count; #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[numLayers]; for (int m = 0; m < numLayers; ++m) { terrainLayers[m] = new TerrainLayer(); HEU_LoadBufferVolumeLayer layer = terrainBuffers[t]._layers[m]; if (!string.IsNullOrEmpty(layer._diffuseTexturePath)) { // Using Resources.Load is much faster than AssetDatabase.Load //terrainLayers[m].diffuseTexture = HEU_MaterialFactory.LoadTexture(layer._diffuseTexturePath); terrainLayers[m].diffuseTexture = Resources.Load<Texture2D>(layer._diffuseTexturePath); } if (terrainLayers[m].diffuseTexture == null) { terrainLayers[m].diffuseTexture = defaultTexture; } terrainLayers[m].diffuseRemapMin = Vector4.zero; terrainLayers[m].diffuseRemapMax = Vector4.one; if (!string.IsNullOrEmpty(layer._maskTexturePath)) { // Using Resources.Load is much faster than AssetDatabase.Load //terrainLayers[m].maskMapTexture = HEU_MaterialFactory.LoadTexture(layer._maskTexturePath); terrainLayers[m].maskMapTexture = Resources.Load<Texture2D>(layer._maskTexturePath); } terrainLayers[m].maskMapRemapMin = Vector4.zero; terrainLayers[m].maskMapRemapMax = Vector4.one; terrainLayers[m].metallic = layer._metallic; if (!string.IsNullOrEmpty(layer._normalTexturePath)) { terrainLayers[m].normalMapTexture = HEU_MaterialFactory.LoadTexture(layer._normalTexturePath); } 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 && terrainLayers[m].diffuseTexture != null) { // Use texture size if tile size is 0 layer._tileSize = new Vector2(terrainLayers[m].diffuseTexture.width, terrainLayers[m].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[numLayers]; for (int m = 0; m < numLayers; ++m) { splatPrototypes[m] = new SplatPrototype(); HEU_LoadBufferVolumeLayer layer = terrainBuffers[t]._layers[m]; Texture2D diffuseTexture = null; if (!string.IsNullOrEmpty(layer._diffuseTexturePath)) { diffuseTexture = HEU_MaterialFactory.LoadTexture(layer._diffuseTexturePath); } if (diffuseTexture == null) { diffuseTexture = defaultTexture; } splatPrototypes[m].texture = diffuseTexture; splatPrototypes[m].tileOffset = layer._tileOffset; if (layer._tileSize.magnitude == 0f && diffuseTexture != null) { // Use texture size if tile size is 0 layer._tileSize = new Vector2(diffuseTexture.width, diffuseTexture.height); } splatPrototypes[m].tileSize = layer._tileSize; splatPrototypes[m].metallic = layer._metallic; splatPrototypes[m].smoothness = layer._smoothness; if (!string.IsNullOrEmpty(layer._normalTexturePath)) { splatPrototypes[m].normalMap = HEU_MaterialFactory.LoadTexture(layer._normalTexturePath); } } terrainData.splatPrototypes = splatPrototypes; #endif terrainData.SetAlphamaps(0, 0, terrainBuffers[t]._splatMaps); //string assetPath = HEU_AssetDatabase.CreateAssetCacheFolder("terrainData"); //AssetDatabase.CreateAsset(terrainData, assetPath); //Debug.Log("Created asset data at " + assetPath); terrainBuffers[t]._generatedOutput = generatedOutput; _generatedOutputs.Add(generatedOutput); SetOutputVisiblity(terrainBuffers[t]); } } }
public static bool GenerateMeshUsingGeoCache(HEU_SessionBase session, HEU_HoudiniAsset asset, GameObject gameObject, HEU_GenerateGeoCache geoCache, bool bGenerateUVs, bool bGenerateTangents, bool bPartInstanced) { #if HEU_PROFILER_ON float generateMeshTime = Time.realtimeSinceStartup; #endif string collisionGroupName = HEU_PluginSettings.CollisionGroupName; string renderCollisionGroupName = HEU_PluginSettings.RenderedCollisionGroupName; // Stores submesh data based on material key (ie. a submesh for each unique material) // Unity requires that if using multiple materials in the same GameObject, then we // need to create corresponding number of submeshes as materials. // So we'll create a submesh for each material in use. // Each submesh will have a list of vertices and their attributes which // we'll collect in a helper class (HEU_MeshData). // Once we collected all the submesh data, we create a CombineInstance for each // submesh, then combine it while perserving the submeshes. Dictionary<int, HEU_MeshData> subMeshesMap = new Dictionary<int, HEU_MeshData>(); string defaultMaterialName = HEU_HoudiniAsset.GenerateDefaultMaterialName(geoCache.GeoID, geoCache.PartID); int defaultMaterialKey = HEU_MaterialFactory.MaterialNameToKey(defaultMaterialName); int singleFaceUnityMaterialKey = HEU_Defines.HEU_INVALID_MATERIAL; int singleFaceHoudiniMaterialKey = HEU_Defines.HEU_INVALID_MATERIAL; // Now go through each group data and acquire the vertex data. // We'll create the collider mesh rightaway and assign to the gameobject. int numCollisionMeshes = 0; foreach (KeyValuePair<string, int[]> groupSplitFacesPair in geoCache._groupSplitVertexIndices) { string groupName = groupSplitFacesPair.Key; int[] groupVertexList = groupSplitFacesPair.Value; bool bIsCollidable = groupName.Contains(collisionGroupName); bool bIsRenderCollidable = groupName.Contains(renderCollisionGroupName); if (bIsCollidable || bIsRenderCollidable) { if (numCollisionMeshes > 0) { Debug.LogWarningFormat("More than 1 collision mesh detected for part {0}.\nOnly a single collision mesh is supported per part.", geoCache._partName); } if (geoCache._partInfo.type == HAPI_PartType.HAPI_PARTTYPE_BOX) { // Box collider HAPI_BoxInfo boxInfo = new HAPI_BoxInfo(); if (session.GetBoxInfo(geoCache.GeoID, geoCache.PartID, ref boxInfo)) { BoxCollider boxCollider = HEU_GeneralUtility.GetOrCreateComponent<BoxCollider>(gameObject); boxCollider.center = new Vector3(-boxInfo.center[0], boxInfo.center[1], boxInfo.center[2]); boxCollider.size = new Vector3(boxInfo.size[0] * 2f, boxInfo.size[1] * 2f, boxInfo.size[2] * 2f); // TODO: Should we apply the box info rotation here to the box collider? // If so, it should be in its own gameobject? } } else if (geoCache._partInfo.type == HAPI_PartType.HAPI_PARTTYPE_SPHERE) { // Sphere collider HAPI_SphereInfo sphereInfo = new HAPI_SphereInfo(); if (session.GetSphereInfo(geoCache.GeoID, geoCache.PartID, ref sphereInfo)) { SphereCollider sphereCollider = HEU_GeneralUtility.GetOrCreateComponent<SphereCollider>(gameObject); sphereCollider.center = new Vector3(-sphereInfo.center[0], sphereInfo.center[1], sphereInfo.center[2]); sphereCollider.radius = sphereInfo.radius; } } else { // Mesh collider List<Vector3> collisionVertices = new List<Vector3>(); for (int v = 0; v < groupVertexList.Length; ++v) { int index = groupVertexList[v]; if (index >= 0 && index < geoCache._posAttr.Length) { collisionVertices.Add(new Vector3(-geoCache._posAttr[index * 3], geoCache._posAttr[index * 3 + 1], geoCache._posAttr[index * 3 + 2])); } } int[] collisionIndices = new int[collisionVertices.Count]; for (int i = 0; i < collisionIndices.Length; ++i) { collisionIndices[i] = i; } Mesh collisionMesh = new Mesh(); #if UNITY_2017_3_OR_NEWER collisionMesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; #endif collisionMesh.name = groupName; collisionMesh.vertices = collisionVertices.ToArray(); collisionMesh.triangles = collisionIndices; collisionMesh.RecalculateBounds(); MeshCollider meshCollider = HEU_GeneralUtility.GetOrCreateComponent<MeshCollider>(gameObject); meshCollider.sharedMesh = collisionMesh; } numCollisionMeshes++; } if (bIsCollidable && !bIsRenderCollidable) { continue; } // After this point, we'll be only processing renderable geometry // Transfer indices for each attribute from the single large list into group lists float[] groupColorAttr = new float[0]; HEU_GenerateGeoCache.TransferRegularPointAttributesToVertices(groupVertexList, ref geoCache._colorAttrInfo, geoCache._colorAttr, ref groupColorAttr); float[] groupAlphaAttr = new float[0]; HEU_GenerateGeoCache.TransferRegularPointAttributesToVertices(groupVertexList, ref geoCache._alphaAttrInfo, geoCache._alphaAttr, ref groupAlphaAttr); float[] groupNormalAttr = new float[0]; HEU_GenerateGeoCache.TransferRegularPointAttributesToVertices(groupVertexList, ref geoCache._normalAttrInfo, geoCache._normalAttr, ref groupNormalAttr); float[] groupTangentsAttr = new float[0]; HEU_GenerateGeoCache.TransferRegularPointAttributesToVertices(groupVertexList, ref geoCache._tangentAttrInfo, geoCache._tangentAttr, ref groupTangentsAttr); float[] groupUVAttr = new float[0]; HEU_GenerateGeoCache.TransferRegularPointAttributesToVertices(groupVertexList, ref geoCache._uvAttrInfo, geoCache._uvAttr, ref groupUVAttr); float[] groupUV2Attr = new float[0]; HEU_GenerateGeoCache.TransferRegularPointAttributesToVertices(groupVertexList, ref geoCache._uv2AttrInfo, geoCache._uv2Attr, ref groupUV2Attr); float[] groupUV3Attr = new float[0]; HEU_GenerateGeoCache.TransferRegularPointAttributesToVertices(groupVertexList, ref geoCache._uv3AttrInfo, geoCache._uv3Attr, ref groupUV3Attr); // Unity mesh creation requires # of vertices must equal # of attributes (color, normal, uvs). // HAPI gives us point indices. Since our attributes are via vertex, we need to therefore // create new indices of vertices that correspond to our attributes. // To reindex, we go through each index, add each attribute corresponding to that index to respective lists. // Then we set the index of where we added those attributes as the new index. int numIndices = groupVertexList.Length; for (int vertexIndex = 0; vertexIndex < numIndices; vertexIndex += 3) { // groupVertexList contains -1 for unused indices, and > 0 for used if (groupVertexList[vertexIndex] == -1) { continue; } int faceIndex = vertexIndex / 3; int faceMaterialID = geoCache._houdiniMaterialIDs[faceIndex]; // Get the submesh ID for this face. Depends on whether it is a Houdini or Unity material. // Using default material as failsafe int submeshID = HEU_Defines.HEU_INVALID_MATERIAL; if (geoCache._unityMaterialAttrInfo.exists) { // This face might have a Unity or Substance material attribute. // Formulate the submesh ID by combining the material attributes. if (geoCache._singleFaceUnityMaterial) { if (singleFaceUnityMaterialKey == HEU_Defines.HEU_INVALID_MATERIAL && geoCache._unityMaterialInfos.Count > 0) { // Use first material var unityMaterialMapEnumerator = geoCache._unityMaterialInfos.GetEnumerator(); if (unityMaterialMapEnumerator.MoveNext()) { singleFaceUnityMaterialKey = unityMaterialMapEnumerator.Current.Key; } } submeshID = singleFaceUnityMaterialKey; } else { int attrIndex = faceIndex; if (geoCache._unityMaterialAttrInfo.owner == HAPI_AttributeOwner.HAPI_ATTROWNER_PRIM || geoCache._unityMaterialAttrInfo.owner == HAPI_AttributeOwner.HAPI_ATTROWNER_POINT) { if (geoCache._unityMaterialAttrInfo.owner == HAPI_AttributeOwner.HAPI_ATTROWNER_POINT) { attrIndex = groupVertexList[vertexIndex]; } string unityMaterialName = ""; string substanceName = ""; int substanceIndex = -1; submeshID = HEU_GenerateGeoCache.GetMaterialKeyFromAttributeIndex(geoCache, attrIndex, out unityMaterialName, out substanceName, out substanceIndex); } else { // (geoCache._unityMaterialAttrInfo.owner == HAPI_AttributeOwner.HAPI_ATTROWNER_DETAIL) should have been handled as geoCache._singleFaceMaterial above Debug.LogErrorFormat("Unity material attribute not supported for attribute type {0}!", geoCache._unityMaterialAttrInfo.owner); } } } if (submeshID == HEU_Defines.HEU_INVALID_MATERIAL) { // Check if has Houdini material assignment if (geoCache._houdiniMaterialIDs.Length > 0) { if (geoCache._singleFaceHoudiniMaterial) { if (singleFaceHoudiniMaterialKey == HEU_Defines.HEU_INVALID_MATERIAL) { singleFaceHoudiniMaterialKey = geoCache._houdiniMaterialIDs[0]; } submeshID = singleFaceHoudiniMaterialKey; } else if (faceMaterialID > 0) { submeshID = faceMaterialID; } } if (submeshID == HEU_Defines.HEU_INVALID_MATERIAL) { // Use default material submeshID = defaultMaterialKey; } } if (!subMeshesMap.ContainsKey(submeshID)) { // New submesh subMeshesMap.Add(submeshID, new HEU_MeshData()); } HEU_MeshData subMeshData = subMeshesMap[submeshID]; for (int triIndex = 0; triIndex < 3; ++triIndex) { int vertexTriIndex = vertexIndex + triIndex; int positionIndex = groupVertexList[vertexTriIndex]; // Position Vector3 position = new Vector3(-geoCache._posAttr[positionIndex * 3 + 0], geoCache._posAttr[positionIndex * 3 + 1], geoCache._posAttr[positionIndex * 3 + 2]); subMeshData._vertices.Add(position); // Color if (geoCache._colorAttrInfo.exists) { Color tempColor = new Color(); tempColor.r = Mathf.Clamp01(groupColorAttr[vertexTriIndex * geoCache._colorAttrInfo.tupleSize + 0]); tempColor.g = Mathf.Clamp01(groupColorAttr[vertexTriIndex * geoCache._colorAttrInfo.tupleSize + 1]); tempColor.b = Mathf.Clamp01(groupColorAttr[vertexTriIndex * geoCache._colorAttrInfo.tupleSize + 2]); if (geoCache._alphaAttrInfo.exists) { tempColor.a = Mathf.Clamp01(groupAlphaAttr[vertexTriIndex]); } else if (geoCache._colorAttrInfo.tupleSize == 4) { tempColor.a = Mathf.Clamp01(groupColorAttr[vertexTriIndex * geoCache._colorAttrInfo.tupleSize + 3]); } else { tempColor.a = 1f; } subMeshData._colors.Add(tempColor); } else { subMeshData._colors.Add(Color.white); } // Normal if (vertexTriIndex < groupNormalAttr.Length) { // Flip the x Vector3 normal = new Vector3(-groupNormalAttr[vertexTriIndex * 3 + 0], groupNormalAttr[vertexTriIndex * 3 + 1], groupNormalAttr[vertexTriIndex * 3 + 2]); subMeshData._normals.Add(normal); } else { // We'll be calculating normals later subMeshData._normals.Add(Vector3.zero); } // UV1 if (vertexTriIndex < groupUVAttr.Length) { Vector2 uv = new Vector2(groupUVAttr[vertexTriIndex * 2 + 0], groupUVAttr[vertexTriIndex * 2 + 1]); subMeshData._UVs.Add(uv); } // UV2 if (vertexTriIndex < groupUV2Attr.Length) { Vector2 uv = new Vector2(groupUV2Attr[vertexTriIndex * 2 + 0], groupUV2Attr[vertexTriIndex * 2 + 1]); subMeshData._UV2s.Add(uv); } // UV3 if (vertexTriIndex < groupUV3Attr.Length) { Vector2 uv = new Vector2(groupUV3Attr[vertexTriIndex * 2 + 0], groupUV3Attr[vertexTriIndex * 2 + 1]); subMeshData._UV3s.Add(uv); } // Tangents if (bGenerateTangents && vertexTriIndex < groupTangentsAttr.Length) { Vector4 tangent = Vector4.zero; if (geoCache._tangentAttrInfo.tupleSize == 4) { tangent = new Vector4(-groupTangentsAttr[vertexTriIndex * 4 + 0], groupTangentsAttr[vertexTriIndex * 4 + 1], groupTangentsAttr[vertexTriIndex * 4 + 2], groupTangentsAttr[vertexTriIndex * 4 + 3]); } else if (geoCache._tangentAttrInfo.tupleSize == 3) { tangent = new Vector4(-groupTangentsAttr[vertexTriIndex * 3 + 0], groupTangentsAttr[vertexTriIndex * 3 + 1], groupTangentsAttr[vertexTriIndex * 3 + 2], 1); } subMeshData._tangents.Add(tangent); } subMeshData._indices.Add(subMeshData._vertices.Count - 1); //Debug.LogFormat("Submesh index mat {0} count {1}", faceMaterialID, subMeshData._indices.Count); } if (!geoCache._normalAttrInfo.exists) { // To generate normals after all the submeshes have been defined, we // calculate and store each triangle normal, along with the list // of connected vertices for each vertex int triIndex = subMeshData._indices.Count - 3; int i1 = subMeshData._indices[triIndex + 0]; int i2 = subMeshData._indices[triIndex + 1]; int i3 = subMeshData._indices[triIndex + 2]; // Triangle normal Vector3 p1 = subMeshData._vertices[i2] - subMeshData._vertices[i1]; Vector3 p2 = subMeshData._vertices[i3] - subMeshData._vertices[i1]; Vector3 normal = Vector3.Cross(p1, p2).normalized; subMeshData._triangleNormals.Add(normal); int normalIndex = subMeshData._triangleNormals.Count - 1; // Connected vertices geoCache._sharedNormalIndices[groupVertexList[vertexIndex + 0]].Add(new VertexEntry(submeshID, i1, normalIndex)); geoCache._sharedNormalIndices[groupVertexList[vertexIndex + 1]].Add(new VertexEntry(submeshID, i2, normalIndex)); geoCache._sharedNormalIndices[groupVertexList[vertexIndex + 2]].Add(new VertexEntry(submeshID, i3, normalIndex)); } } } int numSubmeshes = subMeshesMap.Keys.Count; bool bGenerated = false; if (numSubmeshes > 0) { if (!geoCache._normalAttrInfo.exists) { // Normal calculation // Go throuch each vertex for the entire geometry and calculate the normal vector based on connected // vertices. This includes vertex connections between submeshes so we should get smooth transitions across submeshes. int numSharedNormals = geoCache._sharedNormalIndices.Length; for (int a = 0; a < numSharedNormals; ++a) { for (int b = 0; b < geoCache._sharedNormalIndices[a].Count; ++b) { Vector3 sumNormal = new Vector3(); VertexEntry leftEntry = geoCache._sharedNormalIndices[a][b]; HEU_MeshData leftSubMesh = subMeshesMap[leftEntry._meshKey]; List<VertexEntry> rightList = geoCache._sharedNormalIndices[a]; for (int c = 0; c < rightList.Count; ++c) { VertexEntry rightEntry = rightList[c]; HEU_MeshData rightSubMesh = subMeshesMap[rightEntry._meshKey]; if (leftEntry._vertexIndex == rightEntry._vertexIndex) { sumNormal += rightSubMesh._triangleNormals[rightEntry._normalIndex]; } else { float dot = Vector3.Dot(leftSubMesh._triangleNormals[leftEntry._normalIndex], rightSubMesh._triangleNormals[rightEntry._normalIndex]); if (dot >= geoCache._cosineThreshold) { sumNormal += rightSubMesh._triangleNormals[rightEntry._normalIndex]; } } } leftSubMesh._normals[leftEntry._vertexIndex] = sumNormal.normalized; } } } // Go through each valid submesh data and upload into a CombineInstance for combining. // Each CombineInstance represents a submesh in the final mesh. // And each submesh in that final mesh corresponds to a material. // Filter out only the submeshes with valid geometry List<Material> validMaterials = new List<Material>(); List<int> validSubmeshes = new List<int>(); Dictionary<int, HEU_MaterialData> assetMaterialMap = asset.GetMaterialDataMap(); foreach (KeyValuePair<int, HEU_MeshData> meshPair in subMeshesMap) { HEU_MeshData meshData = meshPair.Value; if (meshData._indices.Count > 0) { int materialKey = meshPair.Key; // Find the material or create it HEU_MaterialData materialData = null; HEU_UnityMaterialInfo unityMaterialInfo = null; if (geoCache._unityMaterialInfos.TryGetValue(materialKey, out unityMaterialInfo)) { if (!assetMaterialMap.TryGetValue(materialKey, out materialData)) { // Create the material materialData = asset.CreateUnitySubstanceMaterialData(materialKey, unityMaterialInfo._unityMaterialPath, unityMaterialInfo._substancePath, unityMaterialInfo._substanceIndex); assetMaterialMap.Add(materialData._materialKey, materialData); } } else if (!assetMaterialMap.TryGetValue(materialKey, out materialData)) { if (materialKey == defaultMaterialKey) { materialData = asset.GetOrCreateDefaultMaterialInCache(session, geoCache.GeoID, geoCache.PartID, false); } else { materialData = asset.CreateHoudiniMaterialData(session, materialKey, geoCache.GeoID, geoCache.PartID); } } if (materialData != null) { validSubmeshes.Add(meshPair.Key); validMaterials.Add(materialData._material); if (materialData != null && bPartInstanced) { // Handle GPU instancing on material for instanced meshes if (materialData._materialSource != HEU_MaterialData.Source.UNITY && materialData._materialSource != HEU_MaterialData.Source.SUBSTANCE) { // Always enable GPU instancing for material generated from Houdini HEU_MaterialFactory.EnableGPUInstancing(materialData._material); } } } } } int validNumSubmeshes = validSubmeshes.Count; CombineInstance[] meshCombiner = new CombineInstance[validNumSubmeshes]; for (int submeshIndex = 0; submeshIndex < validNumSubmeshes; ++submeshIndex) { HEU_MeshData submesh = subMeshesMap[validSubmeshes[submeshIndex]]; CombineInstance combine = new CombineInstance(); combine.mesh = new Mesh(); #if UNITY_2017_3_OR_NEWER combine.mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; #endif combine.mesh.SetVertices(submesh._vertices); combine.mesh.SetIndices(submesh._indices.ToArray(), MeshTopology.Triangles, 0); if (submesh._colors.Count > 0) { combine.mesh.SetColors(submesh._colors); } if (submesh._normals.Count > 0) { combine.mesh.SetNormals(submesh._normals); } if (submesh._tangents.Count > 0) { combine.mesh.SetTangents(submesh._tangents); } if (bGenerateUVs) { // TODO: revisit to test this out Vector2[] generatedUVs = HEU_GeometryUtility.GeneratePerTriangle(combine.mesh); if (generatedUVs != null) { combine.mesh.uv = generatedUVs; } } else if (submesh._UVs.Count > 0) { combine.mesh.SetUVs(0, submesh._UVs); } if (submesh._UV2s.Count > 0) { combine.mesh.SetUVs(1, submesh._UV2s); } if (submesh._UV3s.Count > 0) { combine.mesh.SetUVs(2, submesh._UV3s); } combine.transform = Matrix4x4.identity; combine.mesh.RecalculateBounds(); //Debug.LogFormat("Number of submeshes {0}", combine.mesh.subMeshCount); meshCombiner[submeshIndex] = combine; } // Geometry data MeshFilter meshFilter = HEU_GeneralUtility.GetOrCreateComponent<MeshFilter>(gameObject); meshFilter.sharedMesh = new Mesh(); #if UNITY_2017_3_OR_NEWER meshFilter.sharedMesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; #endif meshFilter.sharedMesh.name = geoCache._partName + "_mesh"; meshFilter.sharedMesh.CombineMeshes(meshCombiner, false, false); meshFilter.sharedMesh.RecalculateBounds(); if (!geoCache._tangentAttrInfo.exists && bGenerateTangents) { HEU_GeometryUtility.CalculateMeshTangents(meshFilter.sharedMesh); } meshFilter.sharedMesh.UploadMeshData(true); // Render data MeshRenderer meshRenderer = HEU_GeneralUtility.GetOrCreateComponent<MeshRenderer>(gameObject); meshRenderer.sharedMaterials = validMaterials.ToArray(); bGenerated = true; } #if HEU_PROFILER_ON Debug.LogFormat("GENERATE MESH TIME:: {0}", (Time.realtimeSinceStartup - generateMeshTime)); #endif return bGenerated; }