using System; using System.IO; using UnityEditor; using UnityEngine; public class TerrainBakerGPU : EditorWindow { private Terrain targetTerrain; private int textureResolution = 2048; private string savePath = "Assets/BakedTerrainTextures"; private bool bakeAlbedo = true; private bool bakeNormal = true; private bool bakeHeightmap = false; private float normalStrength = 1.0f; private bool useGPU = true; private float gpuGammaCorrection = 1.1f; private Texture2D previewAlbedo; private Texture2D previewNormal; private Vector2 scrollPosition; private Shader albedoBakerShader; private const string ALBEDO_BAKER_SHADER_PATH = "Assets/Editor/Tools/TerrainBaker/TerrainBakerShader.shader"; private const string ALBEDO_BAKER_SHADER_NAME = "Hidden/TerrainBaker/AlbedoBaker"; [MenuItem("Tools/*地形烘焙工具/Terrain贴图烘焙(GPU加速)")] public static void ShowWindow() { var window = GetWindow("Terrain贴图烘焙(GPU)"); window.minSize = new Vector2(450, 750); } private void OnEnable() { if (targetTerrain == null) { targetTerrain = FindObjectOfType(); } LoadBakerShader(); } private void OnDisable() { CleanupPreview(); } private void CleanupPreview() { if (previewAlbedo != null) { DestroyImmediate(previewAlbedo); previewAlbedo = null; } if (previewNormal != null) { DestroyImmediate(previewNormal); previewNormal = null; } } private void LoadBakerShader() { albedoBakerShader = Shader.Find(ALBEDO_BAKER_SHADER_NAME); if (albedoBakerShader == null) { albedoBakerShader = AssetDatabase.LoadAssetAtPath(ALBEDO_BAKER_SHADER_PATH); } if (albedoBakerShader == null) { Debug.LogWarning($"无法加载GPU烘焙Shader,将自动使用CPU模式。\n尝试加载: {ALBEDO_BAKER_SHADER_NAME}\n路径: {ALBEDO_BAKER_SHADER_PATH}"); useGPU = false; } } private void OnGUI() { scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); DrawHeader(); DrawTerrainSettings(); DrawLayerInfo(); DrawShaderSettings(); DrawBakeOptions(); DrawOutputSettings(); DrawPreview(); DrawActionButtons(); EditorGUILayout.EndScrollView(); } private void DrawHeader() { GUILayout.Space(10); GUIStyle headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 16, alignment = TextAnchor.MiddleCenter }; GUILayout.Label("Terrain 贴图烘焙工具", headerStyle); GUILayout.Space(5); string modeText = useGPU ? "GPU模式" : "CPU模式"; EditorGUILayout.HelpBox($"当前: {modeText}\n将Terrain的材质贴图和法线信息烘焙成单张纹理。", MessageType.Info); GUILayout.Space(10); } private void DrawTerrainSettings() { EditorGUILayout.LabelField("目标Terrain", EditorStyles.boldLabel); EditorGUI.BeginChangeCheck(); targetTerrain = (Terrain)EditorGUILayout.ObjectField("Terrain对象", targetTerrain, typeof(Terrain), true); if (EditorGUI.EndChangeCheck()) { CleanupPreview(); } if (targetTerrain != null) { EditorGUI.indentLevel++; TerrainData terrainData = targetTerrain.terrainData; EditorGUILayout.LabelField("Terrain尺寸", $"{terrainData.size.x} x {terrainData.size.z} x {terrainData.size.y}"); EditorGUILayout.LabelField("高度图分辨率", terrainData.heightmapResolution.ToString()); EditorGUILayout.LabelField("Alpha图分辨率", $"{terrainData.alphamapWidth} x {terrainData.alphamapHeight}"); EditorGUILayout.LabelField("Splat层数", terrainData.terrainLayers?.Length.ToString() ?? "0"); EditorGUI.indentLevel--; } else { EditorGUILayout.HelpBox("请选择一个Terrain对象", MessageType.Warning); } GUILayout.Space(10); } private void DrawLayerInfo() { if (targetTerrain == null) return; TerrainData terrainData = targetTerrain.terrainData; TerrainLayer[] layers = terrainData.terrainLayers; if (layers == null || layers.Length == 0) return; EditorGUILayout.LabelField("Terrain层信息", EditorStyles.boldLabel); EditorGUI.indentLevel++; for (int i = 0; i < layers.Length; i++) { if (layers[i] == null) continue; EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField($"Layer {i}: {layers[i].name}", GUILayout.Width(200)); if (layers[i].diffuseTexture != null) { GUILayout.Box(layers[i].diffuseTexture, GUILayout.Width(32), GUILayout.Height(32)); } if (layers[i].normalMapTexture != null) { EditorGUILayout.LabelField("✓ Normal", GUILayout.Width(60)); } else { EditorGUILayout.LabelField("✗ Normal", GUILayout.Width(60)); } EditorGUILayout.LabelField($"Tile: {layers[i].tileSize.x}x{layers[i].tileSize.y}"); EditorGUILayout.EndHorizontal(); } EditorGUI.indentLevel--; GUILayout.Space(10); } private void DrawShaderSettings() { EditorGUILayout.LabelField("烘焙模式", EditorStyles.boldLabel); EditorGUI.BeginChangeCheck(); useGPU = EditorGUILayout.Toggle("使用GPU加速", useGPU); if (EditorGUI.EndChangeCheck()) { if (useGPU && albedoBakerShader == null) { LoadBakerShader(); if (albedoBakerShader == null) { EditorUtility.DisplayDialog("提示", "无法加载GPU烘焙Shader,请检查shader文件是否存在。\n" + $"路径: {ALBEDO_BAKER_SHADER_PATH}", "确定"); useGPU = false; } } } if (useGPU) { EditorGUI.indentLevel++; if (albedoBakerShader != null) { EditorGUILayout.LabelField("Shader状态", "✓ 已加载", EditorStyles.miniLabel); } else { EditorGUILayout.LabelField("Shader状态", "✗ 未加载", EditorStyles.miniLabel); } EditorGUI.BeginChangeCheck(); albedoBakerShader = (Shader)EditorGUILayout.ObjectField("Albedo Baker Shader", albedoBakerShader, typeof(Shader), false); if (EditorGUI.EndChangeCheck()) { if (albedoBakerShader == null) { useGPU = false; } } if (GUILayout.Button("重新加载Shader", GUILayout.Height(20))) { LoadBakerShader(); if (albedoBakerShader != null) { EditorUtility.DisplayDialog("成功", "Shader加载成功!", "确定"); } else { EditorUtility.DisplayDialog("失败", "Shader加载失败,请检查文件是否存在。", "确定"); useGPU = false; } } gpuGammaCorrection = EditorGUILayout.Slider("Gamma校正", gpuGammaCorrection, 1.0f, 1.5f); EditorGUILayout.LabelField(" (1.0=无校正, 较大值=更暗)", EditorStyles.miniLabel); EditorGUI.indentLevel--; if (targetTerrain != null && targetTerrain.terrainData.terrainLayers != null && targetTerrain.terrainData.terrainLayers.Length > 4) { EditorGUILayout.HelpBox("当前Terrain超过4层,GPU模式将分批处理。", MessageType.Info); } } else { EditorGUILayout.HelpBox("CPU模式:兼容性好但速度较慢。", MessageType.None); } GUILayout.Space(10); } private void DrawBakeOptions() { EditorGUILayout.LabelField("烘焙选项", EditorStyles.boldLabel); bakeAlbedo = EditorGUILayout.Toggle("烘焙漫反射贴图 (Albedo)", bakeAlbedo); bakeNormal = EditorGUILayout.Toggle("烘焙法线贴图 (Normal)", bakeNormal); if (bakeNormal) { EditorGUI.indentLevel++; normalStrength = EditorGUILayout.Slider("法线强度", normalStrength, 0.1f, 2.0f); EditorGUI.indentLevel--; } bakeHeightmap = EditorGUILayout.Toggle("烘焙高度图 (Heightmap)", bakeHeightmap); GUILayout.Space(10); } private void DrawOutputSettings() { EditorGUILayout.LabelField("输出设置", EditorStyles.boldLabel); textureResolution = EditorGUILayout.IntPopup("输出分辨率", textureResolution, new string[] { "512", "1024", "2048", "4096", "8192" }, new int[] { 512, 1024, 2048, 4096, 8192 }); EditorGUILayout.BeginHorizontal(); savePath = EditorGUILayout.TextField("保存路径", savePath); if (GUILayout.Button("...", GUILayout.Width(30))) { string selectedPath = EditorUtility.OpenFolderPanel("选择保存目录", Application.dataPath, ""); if (!string.IsNullOrEmpty(selectedPath)) { if (selectedPath.StartsWith(Application.dataPath)) { savePath = "Assets" + selectedPath.Substring(Application.dataPath.Length); } else { savePath = selectedPath; } } } EditorGUILayout.EndHorizontal(); GUILayout.Space(10); } private void DrawPreview() { EditorGUILayout.LabelField("预览", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); if (previewAlbedo != null) { EditorGUILayout.BeginVertical(GUILayout.Width(position.width / 2 - 20)); GUILayout.Label("Albedo预览", EditorStyles.centeredGreyMiniLabel); float previewSize = Mathf.Min(150, position.width / 2 - 30); GUILayout.Box(previewAlbedo, GUILayout.Width(previewSize), GUILayout.Height(previewSize)); EditorGUILayout.EndVertical(); } if (previewNormal != null) { EditorGUILayout.BeginVertical(GUILayout.Width(position.width / 2 - 20)); GUILayout.Label("Normal预览", EditorStyles.centeredGreyMiniLabel); float previewSize = Mathf.Min(150, position.width / 2 - 30); GUILayout.Box(previewNormal, GUILayout.Width(previewSize), GUILayout.Height(previewSize)); EditorGUILayout.EndVertical(); } EditorGUILayout.EndHorizontal(); GUILayout.Space(10); } private void DrawActionButtons() { EditorGUILayout.LabelField("操作", EditorStyles.boldLabel); GUI.enabled = targetTerrain != null; EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("生成预览", GUILayout.Height(30))) { GeneratePreview(); } if (GUILayout.Button("烘焙并保存", GUILayout.Height(30))) { BakeAndSave(); } EditorGUILayout.EndHorizontal(); GUILayout.Space(5); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("选中场景中的Terrain", GUILayout.Height(25))) { targetTerrain = FindObjectOfType(); if (targetTerrain != null) { Selection.activeGameObject = targetTerrain.gameObject; } } if (GUILayout.Button("清除预览", GUILayout.Height(25))) { CleanupPreview(); } EditorGUILayout.EndHorizontal(); GUI.enabled = true; GUILayout.Space(10); } private void GeneratePreview() { if (targetTerrain == null) { Debug.LogError("请先选择一个Terrain对象!"); return; } CleanupPreview(); try { EditorUtility.DisplayProgressBar("生成预览", "正在生成预览...", 0.5f); if (bakeAlbedo) { previewAlbedo = BakeAlbedoTexture(256); } if (bakeNormal) { previewNormal = BakeNormalTexture(256); } EditorUtility.ClearProgressBar(); Debug.Log("预览生成完成!"); } catch (Exception ex) { EditorUtility.ClearProgressBar(); Debug.LogError($"生成预览时出错: {ex.Message}\n{ex.StackTrace}"); } } private void BakeAndSave() { if (targetTerrain == null) { EditorUtility.DisplayDialog("错误", "请先选择一个Terrain对象!", "确定"); return; } if (!bakeAlbedo && !bakeNormal && !bakeHeightmap) { EditorUtility.DisplayDialog("错误", "请至少选择一种贴图类型进行烘焙!", "确定"); return; } try { if (!Directory.Exists(savePath)) { Directory.CreateDirectory(savePath); } string terrainName = targetTerrain.name; int savedCount = 0; if (bakeAlbedo) { EditorUtility.DisplayProgressBar("烘焙进度", "正在烘焙漫反射贴图...", 0.2f); Texture2D albedoTex = BakeAlbedoTexture(textureResolution); string albedoPath = Path.Combine(savePath, $"{terrainName}_Albedo.png"); SaveTexture(albedoTex, albedoPath); DestroyImmediate(albedoTex); savedCount++; Debug.Log($"漫反射贴图已保存到: {albedoPath}"); } if (bakeNormal) { EditorUtility.DisplayProgressBar("烘焙进度", "正在烘焙法线贴图...", 0.5f); Texture2D normalTex = BakeNormalTexture(textureResolution); string normalPath = Path.Combine(savePath, $"{terrainName}_Normal.png"); SaveTexture(normalTex, normalPath, true); DestroyImmediate(normalTex); savedCount++; Debug.Log($"法线贴图已保存到: {normalPath}"); } if (bakeHeightmap) { EditorUtility.DisplayProgressBar("烘焙进度", "正在烘焙高度图...", 0.8f); Texture2D heightTex = BakeHeightmapTexture(textureResolution); string heightPath = Path.Combine(savePath, $"{terrainName}_Height.png"); SaveTexture(heightTex, heightPath); DestroyImmediate(heightTex); savedCount++; Debug.Log($"高度图已保存到: {heightPath}"); } EditorUtility.ClearProgressBar(); AssetDatabase.Refresh(); EditorUtility.DisplayDialog("完成", $"成功烘焙并保存了 {savedCount} 张贴图到:\n{savePath}", "确定"); } catch (Exception ex) { EditorUtility.ClearProgressBar(); Debug.LogError($"烘焙时出错: {ex.Message}\n{ex.StackTrace}"); EditorUtility.DisplayDialog("错误", $"烘焙时出错: {ex.Message}", "确定"); } } private Texture2D BakeAlbedoTexture(int resolution) { TerrainData terrainData = targetTerrain.terrainData; TerrainLayer[] terrainLayers = terrainData.terrainLayers; if (terrainLayers == null || terrainLayers.Length == 0) { Debug.LogWarning("Terrain没有设置任何Layer,将生成空白贴图"); return CreateBlankTexture(resolution, Color.gray); } if (useGPU && albedoBakerShader != null) { return BakeAlbedoWithGPU(resolution, terrainData, terrainLayers); } else { return BakeAlbedoWithCPU(resolution, terrainData, terrainLayers); } } private Texture2D BakeAlbedoWithGPU(int resolution, TerrainData terrainData, TerrainLayer[] terrainLayers) { int layerCount = terrainLayers.Length; int alphamapCount = terrainData.alphamapTextureCount; Vector3 terrainSize = terrainData.size; RenderTexture resultRT = RenderTexture.GetTemporary(resolution, resolution, 0, RenderTextureFormat.ARGB32); RenderTexture tempRT = RenderTexture.GetTemporary(resolution, resolution, 0, RenderTextureFormat.ARGB32); RenderTexture.active = resultRT; GL.Clear(true, true, Color.black); try { Material bakerMat = new Material(albedoBakerShader); bakerMat.SetFloat("_GammaCorrection", gpuGammaCorrection); for (int alphamapIndex = 0; alphamapIndex < alphamapCount; alphamapIndex++) { int startLayer = alphamapIndex * 4; Texture2D alphaMap = terrainData.GetAlphamapTexture(alphamapIndex); bakerMat.SetTexture("_Control", alphaMap); bakerMat.SetVector("_TerrainSize", new Vector4(terrainSize.x, terrainSize.z, 0, 0)); for (int i = 0; i < 4; i++) { int layerIndex = startLayer + i; string splatName = "_Splat" + i; string tileName = "_TileSize" + i; if (layerIndex < layerCount && terrainLayers[layerIndex] != null) { TerrainLayer layer = terrainLayers[layerIndex]; Texture2D diffuse = layer.diffuseTexture; if (diffuse == null) { diffuse = Texture2D.whiteTexture; } bakerMat.SetTexture(splatName, diffuse); bakerMat.SetVector(tileName, new Vector4( layer.tileSize.x, layer.tileSize.y, layer.tileOffset.x, layer.tileOffset.y)); } else { bakerMat.SetTexture(splatName, Texture2D.blackTexture); bakerMat.SetVector(tileName, new Vector4(1, 1, 0, 0)); } } RenderTexture.active = tempRT; GL.Clear(true, true, Color.black); Graphics.Blit(Texture2D.whiteTexture, tempRT, bakerMat); if (alphamapIndex == 0) { Graphics.Blit(tempRT, resultRT); } else { RenderTexture.active = resultRT; Texture2D resultTex = new Texture2D(resolution, resolution, TextureFormat.RGBA32, false); resultTex.ReadPixels(new Rect(0, 0, resolution, resolution), 0, 0); resultTex.Apply(); RenderTexture.active = tempRT; Texture2D tempTex = new Texture2D(resolution, resolution, TextureFormat.RGBA32, false); tempTex.ReadPixels(new Rect(0, 0, resolution, resolution), 0, 0); tempTex.Apply(); Color[] resultPixels = resultTex.GetPixels(); Color[] tempPixels = tempTex.GetPixels(); for (int p = 0; p < resultPixels.Length; p++) { resultPixels[p] += tempPixels[p]; } resultTex.SetPixels(resultPixels); resultTex.Apply(); Graphics.Blit(resultTex, resultRT); DestroyImmediate(resultTex); DestroyImmediate(tempTex); } } DestroyImmediate(bakerMat); RenderTexture.active = resultRT; Texture2D result = new Texture2D(resolution, resolution, TextureFormat.RGBA32, false); result.ReadPixels(new Rect(0, 0, resolution, resolution), 0, 0); result.Apply(); return result; } finally { RenderTexture.active = null; RenderTexture.ReleaseTemporary(resultRT); RenderTexture.ReleaseTemporary(tempRT); } } private Texture2D BakeAlbedoWithCPU(int resolution, TerrainData terrainData, TerrainLayer[] terrainLayers) { float[,,] alphaMaps = terrainData.GetAlphamaps(0, 0, terrainData.alphamapWidth, terrainData.alphamapHeight); int alphaWidth = terrainData.alphamapWidth; int alphaHeight = terrainData.alphamapHeight; int layerCount = terrainLayers.Length; Texture2D resultTexture = new Texture2D(resolution, resolution, TextureFormat.RGBA32, false); Color[] pixels = new Color[resolution * resolution]; Texture2D[] layerTextures = new Texture2D[layerCount]; for (int i = 0; i < layerCount; i++) { if (terrainLayers[i] != null && terrainLayers[i].diffuseTexture != null) { layerTextures[i] = MakeTextureReadable(terrainLayers[i].diffuseTexture); } } Vector2[] tileSizes = new Vector2[layerCount]; Vector2[] tileOffsets = new Vector2[layerCount]; for (int i = 0; i < layerCount; i++) { if (terrainLayers[i] != null) { tileSizes[i] = terrainLayers[i].tileSize; tileOffsets[i] = terrainLayers[i].tileOffset; } else { tileSizes[i] = Vector2.one; tileOffsets[i] = Vector2.zero; } } Vector3 terrainSize = terrainData.size; for (int y = 0; y < resolution; y++) { for (int x = 0; x < resolution; x++) { float normX = (float)x / resolution; float normY = (float)y / resolution; float alphaFX = normX * (alphaWidth - 1); float alphaFY = normY * (alphaHeight - 1); int alphaX0 = Mathf.FloorToInt(alphaFX); int alphaY0 = Mathf.FloorToInt(alphaFY); int alphaX1 = Mathf.Min(alphaX0 + 1, alphaWidth - 1); int alphaY1 = Mathf.Min(alphaY0 + 1, alphaHeight - 1); float fracX = alphaFX - alphaX0; float fracY = alphaFY - alphaY0; Color finalColor = Color.black; for (int layer = 0; layer < layerCount; layer++) { float w00 = alphaMaps[alphaY0, alphaX0, layer]; float w10 = alphaMaps[alphaY0, alphaX1, layer]; float w01 = alphaMaps[alphaY1, alphaX0, layer]; float w11 = alphaMaps[alphaY1, alphaX1, layer]; float weight = Mathf.Lerp( Mathf.Lerp(w00, w10, fracX), Mathf.Lerp(w01, w11, fracX), fracY); if (weight > 0.001f && layerTextures[layer] != null) { float worldX = normX * terrainSize.x; float worldZ = normY * terrainSize.z; float u = (worldX / tileSizes[layer].x) + tileOffsets[layer].x; float v = (worldZ / tileSizes[layer].y) + tileOffsets[layer].y; u = u - Mathf.Floor(u); v = v - Mathf.Floor(v); Color layerColor = SampleTexture(layerTextures[layer], u, v); finalColor += layerColor * weight; } } pixels[y * resolution + x] = finalColor; } if (y % 100 == 0) { EditorUtility.DisplayProgressBar("烘焙进度", $"正在烘焙漫反射贴图... ({y}/{resolution})", (float)y / resolution * 0.3f + 0.2f); } } for (int i = 0; i < layerCount; i++) { if (layerTextures[i] != null) { DestroyImmediate(layerTextures[i]); } } resultTexture.SetPixels(pixels); resultTexture.Apply(); return resultTexture; } private Texture2D BakeNormalTexture(int resolution) { TerrainData terrainData = targetTerrain.terrainData; TerrainLayer[] terrainLayers = terrainData.terrainLayers; Texture2D resultTexture = new Texture2D(resolution, resolution, TextureFormat.RGBA32, false); Color[] pixels = new Color[resolution * resolution]; float[,,] alphaMaps = null; int alphaWidth = 0, alphaHeight = 0; int layerCount = terrainLayers?.Length ?? 0; if (terrainLayers != null && terrainLayers.Length > 0) { alphaMaps = terrainData.GetAlphamaps(0, 0, terrainData.alphamapWidth, terrainData.alphamapHeight); alphaWidth = terrainData.alphamapWidth; alphaHeight = terrainData.alphamapHeight; } Texture2D[] layerNormalMaps = new Texture2D[layerCount]; for (int i = 0; i < layerCount; i++) { if (terrainLayers[i] != null && terrainLayers[i].normalMapTexture != null) { layerNormalMaps[i] = MakeTextureReadable(terrainLayers[i].normalMapTexture); } } Vector2[] tileSizes = new Vector2[layerCount]; Vector2[] tileOffsets = new Vector2[layerCount]; for (int i = 0; i < layerCount; i++) { if (terrainLayers[i] != null) { tileSizes[i] = terrainLayers[i].tileSize; tileOffsets[i] = terrainLayers[i].tileOffset; } else { tileSizes[i] = Vector2.one; tileOffsets[i] = Vector2.zero; } } Vector3 terrainSize = terrainData.size; for (int y = 0; y < resolution; y++) { for (int x = 0; x < resolution; x++) { float normX = (float)x / resolution; float normY = (float)y / resolution; Vector3 terrainNormal = terrainData.GetInterpolatedNormal(normX, normY); Vector3 tangentSpaceTerrainNormal = new Vector3(terrainNormal.x, terrainNormal.z, terrainNormal.y); Vector3 detailNormal = new Vector3(0, 0, 1); if (alphaMaps != null) { int alphaX = Mathf.FloorToInt(normX * (alphaWidth - 1)); int alphaY = Mathf.FloorToInt(normY * (alphaHeight - 1)); alphaX = Mathf.Clamp(alphaX, 0, alphaWidth - 1); alphaY = Mathf.Clamp(alphaY, 0, alphaHeight - 1); Vector3 blendedDetailNormal = Vector3.zero; float totalWeight = 0; for (int layer = 0; layer < layerCount; layer++) { float weight = alphaMaps[alphaY, alphaX, layer]; if (weight > 0.001f && layerNormalMaps[layer] != null) { float worldX = normX * terrainSize.x; float worldZ = normY * terrainSize.z; float u = (worldX / tileSizes[layer].x) + tileOffsets[layer].x; float v = (worldZ / tileSizes[layer].y) + tileOffsets[layer].y; u = u - Mathf.Floor(u); v = v - Mathf.Floor(v); Color normalColor = SampleTexture(layerNormalMaps[layer], u, v); Vector3 layerNormal = UnpackNormal(normalColor); blendedDetailNormal += layerNormal * weight; totalWeight += weight; } } if (totalWeight > 0) { detailNormal = blendedDetailNormal.normalized; } } Vector3 finalNormal = BlendNormals(tangentSpaceTerrainNormal, detailNormal, normalStrength); Color normalPixel = PackNormal(finalNormal); pixels[y * resolution + x] = normalPixel; } if (y % 100 == 0) { EditorUtility.DisplayProgressBar("烘焙进度", $"正在烘焙法线贴图... ({y}/{resolution})", (float)y / resolution * 0.3f + 0.5f); } } for (int i = 0; i < layerCount; i++) { if (layerNormalMaps[i] != null) { DestroyImmediate(layerNormalMaps[i]); } } resultTexture.SetPixels(pixels); resultTexture.Apply(); return resultTexture; } private Texture2D BakeHeightmapTexture(int resolution) { TerrainData terrainData = targetTerrain.terrainData; Texture2D resultTexture = new Texture2D(resolution, resolution, TextureFormat.RGBA32, false); Color[] pixels = new Color[resolution * resolution]; for (int y = 0; y < resolution; y++) { for (int x = 0; x < resolution; x++) { float normX = (float)x / resolution; float normY = (float)y / resolution; float height = terrainData.GetInterpolatedHeight(normX, normY) / terrainData.size.y; pixels[y * resolution + x] = new Color(height, height, height, 1); } } resultTexture.SetPixels(pixels); resultTexture.Apply(); return resultTexture; } private Texture2D MakeTextureReadable(Texture2D source) { if (source == null) return null; RenderTexture rt = RenderTexture.GetTemporary(source.width, source.height, 0, RenderTextureFormat.ARGB32); Graphics.Blit(source, rt); RenderTexture previous = RenderTexture.active; RenderTexture.active = rt; Texture2D readable = new Texture2D(source.width, source.height, TextureFormat.RGBA32, false); readable.ReadPixels(new Rect(0, 0, source.width, source.height), 0, 0); readable.Apply(); RenderTexture.active = previous; RenderTexture.ReleaseTemporary(rt); return readable; } private Color SampleTexture(Texture2D texture, float u, float v) { if (texture == null) return Color.gray; int width = texture.width; int height = texture.height; float fx = u * (width - 1); float fy = v * (height - 1); int x0 = Mathf.FloorToInt(fx); int y0 = Mathf.FloorToInt(fy); int x1 = Mathf.Min(x0 + 1, width - 1); int y1 = Mathf.Min(y0 + 1, height - 1); float fracX = fx - x0; float fracY = fy - y0; Color c00 = texture.GetPixel(x0, y0); Color c10 = texture.GetPixel(x1, y0); Color c01 = texture.GetPixel(x0, y1); Color c11 = texture.GetPixel(x1, y1); Color c0 = Color.Lerp(c00, c10, fracX); Color c1 = Color.Lerp(c01, c11, fracX); return Color.Lerp(c0, c1, fracY); } private Vector3 UnpackNormal(Color color) { Vector3 normal; normal.x = color.r * 2 - 1; normal.y = color.g * 2 - 1; normal.z = color.b * 2 - 1; if (Mathf.Abs(color.b - 0.5f) < 0.01f && Mathf.Abs(color.r - 0.5f) < 0.01f) { normal.x = color.a * 2 - 1; normal.y = color.g * 2 - 1; normal.z = Mathf.Sqrt(1 - Mathf.Clamp01(normal.x * normal.x + normal.y * normal.y)); } return normal.normalized; } private Color PackNormal(Vector3 normal) { return new Color( normal.x * 0.5f + 0.5f, normal.y * 0.5f + 0.5f, normal.z * 0.5f + 0.5f, 1 ); } private Vector3 BlendNormals(Vector3 baseNormal, Vector3 detailNormal, float strength) { detailNormal.x *= strength; detailNormal.y *= strength; Vector3 result = new Vector3( baseNormal.x + detailNormal.x, baseNormal.y + detailNormal.y, baseNormal.z * detailNormal.z ); return result.normalized; } private Texture2D CreateBlankTexture(int resolution, Color color) { Texture2D texture = new Texture2D(resolution, resolution, TextureFormat.RGBA32, false); Color[] pixels = new Color[resolution * resolution]; for (int i = 0; i < pixels.Length; i++) { pixels[i] = color; } texture.SetPixels(pixels); texture.Apply(); return texture; } private void SaveTexture(Texture2D texture, string path, bool isNormalMap = false) { byte[] bytes = texture.EncodeToPNG(); File.WriteAllBytes(path, bytes); AssetDatabase.Refresh(); if (isNormalMap && path.StartsWith("Assets")) { TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter; if (importer != null) { importer.textureType = TextureImporterType.NormalMap; importer.SaveAndReimport(); } } } }