////////////////////////////////////////////////////// // Terain To Mesh // Copyright (c) Jason Booth ////////////////////////////////////////////////////// using UnityEngine; using System.Collections; using System.Collections.Generic; using UnityEditor; using JBooth.MicroSplat; namespace JBooth.TerrainToMesh { #if __MICROSPLAT__ && __MICROSPLAT_MESHTERRAIN__ public class TerrainToMesh : EditorWindow { [MenuItem ("Window/MicroSplat/Terrain To Mesh")] public static void ShowWindow () { var window = GetWindow (); if (window != null) { window.Show (); window.Init (); } } public List selectedTerrains = new List (); void Init () { Object [] objs = Selection.GetFiltered (typeof (Terrain), SelectionMode.Editable | SelectionMode.Deep); selectedTerrains.Clear (); for (int i = 0; i < objs.Length; ++i) { Terrain t = objs [i] as Terrain; if (t == null) continue; selectedTerrains.Add (t); } } void OnSelectionChange () { Init (); } public class Settings { public int chunkDiv = 1; public int subDiv = 64; public bool convertMaterial = true; public bool deleteTerrains = false; public bool generateLowResShader = true; public bool addColliders = false; public string prefix = ""; public string postfix = ""; } public Settings settings = new Settings (); static GUIContent CChunkDiv = new GUIContent ("Chunks", "Number of objects to break terrains into per side"); static GUIContent CSubDiv = new GUIContent ("Sub Divisions", "Sub divisions per terrain chunk per side"); static GUIContent CAddColliders = new GUIContent ("Add Colliders", "Add colliders to terrain chunks. This is required for terrain blending"); void OnGUI () { if (selectedTerrains.Count == 0) { EditorGUILayout.HelpBox ("Select terrains with MicroSplat on them to continue", MessageType.Info); return; } for (int i = 0; i < selectedTerrains.Count; ++i) { if (selectedTerrains [0] == null || selectedTerrains [0].GetComponent () == null) { EditorGUILayout.HelpBox ("Terrain must be setup with MicroSplat to convert", MessageType.Error); return; } } settings.chunkDiv = EditorGUILayout.IntSlider (CChunkDiv, settings.chunkDiv, 1, 32); settings.subDiv = EditorGUILayout.IntSlider (CSubDiv, settings.subDiv, 16, 170); settings.addColliders = EditorGUILayout.Toggle (CAddColliders, settings.addColliders); settings.convertMaterial = EditorGUILayout.Toggle ("Convert Material", settings.convertMaterial); settings.generateLowResShader = EditorGUILayout.Toggle ("Generate Separate Shader", settings.generateLowResShader); settings.deleteTerrains = EditorGUILayout.Toggle ("Delete Terrains", settings.deleteTerrains); settings.prefix = EditorGUILayout.TextField ("Naming Prefix", settings.prefix); settings.postfix = EditorGUILayout.TextField ("Naming Postfix", settings.postfix); if (GUILayout.Button ("Convert")) { if (selectedTerrains [0] == null) { Debug.LogError ("Select a terrain with MicroSplat on it."); return; } var mst = selectedTerrains [0].GetComponent (); if (mst == null || mst.templateMaterial == null) { Debug.Log ("Did not find MicroSplatTerrain component and material template on terrain"); } else { string baseDir = JBooth.MicroSplat.MicroSplatUtilities.RelativePathFromAsset (mst.templateMaterial); if (settings.generateLowResShader) { baseDir += "/MeshTerrain/MicroSplatData"; if (!System.IO.Directory.Exists (baseDir)) { System.IO.Directory.CreateDirectory (baseDir); } } for (int i = 0; i < selectedTerrains.Count; ++i) { var meshPath = baseDir + "/" + selectedTerrains [i].name + "_meshes.asset"; Mesh root = new Mesh (); root.name = "Meshes"; AssetDatabase.CreateAsset (root, meshPath); Convert (selectedTerrains [i], baseDir, root, settings); } AssetDatabase.SaveAssets (); AssetDatabase.Refresh(); MicroSplatObject.SyncAll(); } } } static Mesh CreateSubChunk (Terrain t, Vector3 worldStart, Vector2 uvStart, int dx, int dy, int div, Settings s) { int chunkDiv = s.chunkDiv; int subDiv = s.subDiv; float size = t.terrainData.size.x; float pSize = size * 0.5f; pSize /= chunkDiv; string meshName = div > 0 ? t.name + "_" + dx + "_" + dy : t.name; Mesh mesh = new Mesh (); mesh.name = meshName; Vector3 [] vertices = new Vector3 [subDiv * subDiv]; Vector3 [] normals = new Vector3 [vertices.Length]; for (int z = 0; z < subDiv; z++) { float zPos = ((float)z / (subDiv - 1) - .5f) * pSize * 2; for (int x = 0; x < subDiv; x++) { float xPos = ((float)x / (subDiv - 1) - .5f) * pSize * 2; float uvX = (float)x / (float)(subDiv - 1); float uvY = (float)z / (float)(subDiv - 1); uvX /= (float)chunkDiv; uvY /= (float)chunkDiv; uvX += uvStart.x; uvY += uvStart.y; Vector3 worldPos = new Vector3 (xPos + pSize + worldStart.x, 0, zPos + pSize + worldStart.y); worldPos += t.GetPosition (); float yPos = t.SampleHeight (worldPos); Vector3 nm = t.terrainData.GetInterpolatedNormal (xPos + pSize, zPos + pSize); vertices [x + z * subDiv] = new Vector3 (xPos + pSize + worldStart.x, yPos, zPos + pSize + worldStart.y); normals [x + z * subDiv] = new Vector3 (nm.x, nm.y, nm.z); } } Vector2 [] uvs = new Vector2 [vertices.Length]; for (int v = 0; v < subDiv; v++) { for (int u = 0; u < subDiv; u++) { Vector2 uv = new Vector2 ((float)u / (subDiv - 1), (float)v / (subDiv - 1)); uv.x /= chunkDiv; uv.y /= chunkDiv; uv.x += uvStart.x; uv.y += uvStart.y; uvs [u + v * subDiv] = uv; } } int nbFaces = (subDiv - 1) * (subDiv - 1); int [] triangles = new int [nbFaces * 6]; int tidx = 0; for (int face = 0; face < nbFaces; face++) { // Retrieve lower left corner from face ind int i = face % (subDiv - 1) + (face / (subDiv - 1) * subDiv); triangles [tidx++] = i + subDiv; triangles [tidx++] = i + 1; triangles [tidx++] = i; triangles [tidx++] = i + subDiv; triangles [tidx++] = i + subDiv + 1; triangles [tidx++] = i + 1; } mesh.vertices = vertices; mesh.normals = normals; mesh.uv = uvs; mesh.triangles = triangles; mesh.RecalculateNormals (); mesh.RecalculateTangents (); mesh.RecalculateBounds (); return mesh; } static Texture2D SaveHeightTexture (RenderTexture rt, string basePath) { Texture2D tex = new Texture2D (rt.width, rt.height, TextureFormat.R16, true, true); var old = RenderTexture.active; RenderTexture.active = rt; tex.ReadPixels (new Rect (0, 0, tex.width, tex.height), 0, 0); tex.Apply (); RenderTexture.active = old; JBooth.MicroSplat.HDTextureImporter.Write (tex, basePath + "_height", true, true); AssetDatabase.Refresh (); return AssetDatabase.LoadAssetAtPath (basePath + "_height.hdtexture"); } public static void Convert (Terrain t, string baseDir, Mesh root, Settings s) { int chunkDiv = s.chunkDiv; var mst = t.GetComponent (); if (mst == null) { Debug.LogError ("MicroSplat Terrain missing"); return; } var oldInstance = mst.terrain.drawInstanced; mst.terrain.drawInstanced = false; float chunkSkip = t.terrainData.size.x / (float)chunkDiv; GameObject meshes = new GameObject (s.prefix + t.name + s.postfix); meshes.transform.position = t.transform.position; #if __MICROSPLAT_STREAMS__ var origst = mst.GetComponent (); if (origst != null) { var newst = meshes.AddComponent (); EditorUtility.CopySerialized (origst, newst); } #endif MicroSplatMeshTerrain msm = meshes.AddComponent (); msm.perPixelNormal = mst.perPixelNormal; msm.blendMat = mst.blendMat; msm.propData = mst.propData; msm.streamTexture = mst.streamTexture; if (mst.keywordSO.IsKeywordEnabled("_DYNAMICFLOWS") || mst.keywordSO.IsKeywordEnabled("_TERRAINBLENDING")) { msm.terrainDescriptor.heightMap = SaveHeightTexture(mst.terrain.terrainData.heightmapTexture, baseDir + "/" + mst.name); } if (mst.keywordSO.IsKeywordEnabled("_PERPIXNORMAL") || mst.keywordSO.IsKeywordEnabled("_TERRAINBLENDING") || (mst.terrain != null && oldInstance)) { msm.perPixelNormal = MicroSplatTerrainEditor.GenerateTerrainNormalMap(mst); msm.terrainDescriptor.normalMap = msm.perPixelNormal; } #if (VEGETATION_STUDIO || VEGETATION_STUDIO_PRO) msm.vsGrassMap = mst.vsGrassMap; msm.vsShadowMap = mst.vsShadowMap; #endif #if __MICROSPLAT_ALPHAHOLE__ msm.clipMap = mst.clipMap; #endif #if __MICROSPLAT_PROCTEX__ msm.procBiomeMask = mst.procBiomeMask; msm.procBiomeMask2 = mst.procBiomeMask2; msm.procTexCfg = mst.procTexCfg; msm.cavityMap = mst.cavityMap; #endif #if __MICROSPLAT_SCATTER__ msm.scatterMapOverride = mst.scatterMapOverride; #endif #if __MICROSPLAT_GLOBALTEXTURE__ msm.tintMapOverride = mst.tintMapOverride; msm.geoTextureOverride = mst.geoTextureOverride; msm.globalNormalOverride = mst.globalNormalOverride; msm.globalEmisOverride = mst.globalEmisOverride; msm.globalSAOMOverride = mst.globalSAOMOverride; #endif #if __MICROSPLAT_SNOW__ msm.snowMaskOverride = mst.snowMaskOverride; #endif if (s.convertMaterial) { if (s.generateLowResShader) { if (!System.IO.File.Exists (baseDir + "/MicroSplat.mat")) { System.IO.File.Copy(AssetDatabase.GetAssetPath(mst.templateMaterial), baseDir + "/MicroSplat.mat"); } if (!System.IO.File.Exists (baseDir + "/MicroSplat_keywords.asset")) { System.IO.File.Copy(AssetDatabase.GetAssetPath (mst.keywordSO), baseDir + "/MicroSplat_keywords.asset"); } if (!System.IO.File.Exists (baseDir + "/MicroSplat.shader")) { System.IO.File.Copy(AssetDatabase.GetAssetPath (mst.templateMaterial.shader), baseDir + "/MicroSplat.shader"); } AssetDatabase.Refresh (); msm.templateMaterial = AssetDatabase.LoadAssetAtPath (baseDir + "/MicroSplat.mat"); msm.templateMaterial.shader = AssetDatabase.LoadAssetAtPath (baseDir + "/MicroSplat.shader"); msm.keywordSO = AssetDatabase.LoadAssetAtPath (baseDir + "/MicroSplat_keywords.asset"); msm.templateMaterial.CopyPropertiesFromMaterial (mst.templateMaterial); if (!msm.keywordSO.IsKeywordEnabled ("_MICROMESHTERRAIN")) { msm.keywordSO.EnableKeyword ("_MICROMESHTERRAIN"); } if (msm.keywordSO.IsKeywordEnabled ("_OUTPUTDIGGER")) { msm.keywordSO.DisableKeyword ("_OUTPUTDIGGER"); } if (msm.keywordSO.IsKeywordEnabled ("_MICRODIGGERMESH")) { msm.keywordSO.DisableKeyword ("_MICRODIGGERMESH"); } if (oldInstance) { msm.keywordSO.EnableKeyword ("_PERPIXNORMAL"); } if (msm.keywordSO.IsKeywordEnabled("_MICROTERRAIN")) { msm.keywordSO.DisableKeyword("_MICROTERRAIN"); } MicroSplatShaderGUI.MicroSplatCompiler c = new MicroSplatShaderGUI.MicroSplatCompiler (); c.Compile (msm.templateMaterial); } } // export splat textures var textures = t.terrainData.alphamapTextures; for (int i = 0; i < textures.Length; ++i) { var path = baseDir + "/splat_" + t.name + i.ToString (); var bytes = textures [i].EncodeToTGA (); System.IO.File.WriteAllBytes (path + ".tga", bytes); AssetDatabase.Refresh (); var ai = AssetImporter.GetAtPath (MicroSplat.MicroSplatUtilities.MakeRelativePath (path + ".tga")); var ti = ai as TextureImporter; if (ti != null) { var ps = ti.GetDefaultPlatformTextureSettings (); if (ti.isReadable == true || ti.wrapMode != TextureWrapMode.Clamp || ps.format != TextureImporterFormat.RGBA32 || ps.textureCompression != TextureImporterCompression.Compressed || ps.overridden != true || ti.filterMode != FilterMode.Bilinear || ti.sRGBTexture != false) { ti.sRGBTexture = false; ti.filterMode = FilterMode.Bilinear; ti.mipmapEnabled = true; ti.wrapMode = TextureWrapMode.Clamp; ti.isReadable = false; ps.format = TextureImporterFormat.Automatic; ps.textureCompression = TextureImporterCompression.Compressed; ps.overridden = false; ti.SetPlatformTextureSettings (ps); ti.SaveAndReimport (); } } } msm.controlTextures = new Texture2D [textures.Length]; for (int i = 0; i < textures.Length; ++i) { var p = MicroSplat.MicroSplatUtilities.MakeRelativePath (baseDir + "/splat_" + t.gameObject.name + i + ".tga"); Texture2D tex = AssetDatabase.LoadAssetAtPath (p); msm.controlTextures [i] = tex; } MeshRenderer [] rends = new MeshRenderer [chunkDiv * chunkDiv]; int chunkIdx = 0; for (int chunkX = 0; chunkX < chunkDiv; ++chunkX) { for (int chunkY = 0; chunkY < chunkDiv; ++chunkY) { Vector2 worldStart = new Vector2 (chunkX * chunkSkip, chunkY * chunkSkip); Vector2 uvStart = new Vector2 (worldStart.x / t.terrainData.size.x, worldStart.y / t.terrainData.size.x); Mesh mesh = CreateSubChunk (t, worldStart, uvStart, chunkX, chunkY, chunkDiv, s); AssetDatabase.AddObjectToAsset (mesh, root); GameObject go = new GameObject (t.name + "_" + chunkX + "_" + chunkY); MeshRenderer rend = go.AddComponent (); var filter = go.AddComponent (); filter.sharedMesh = mesh; if (s.addColliders) { var mc = go.AddComponent (); mc.sharedMesh = mesh; } go.transform.position = t.GetPosition (); go.transform.SetParent (meshes.transform); rends [chunkIdx] = rend; chunkIdx++; } } msm.meshTerrains = rends; if (chunkDiv == 1) { // remove parent object var child = msm.transform.GetChild (0).gameObject; var nm = child.AddComponent (); EditorUtility.CopySerialized (msm, nm); child.transform.SetParent (null, true); DestroyImmediate (meshes); nm.Sync (); } else { MicroSplatObject.SyncAll(); } mst.terrain.drawInstanced = oldInstance; if (s.deleteTerrains) { DestroyImmediate (t); } } } #endif }