982 lines
34 KiB
C#
982 lines
34 KiB
C#
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<TerrainBakerGPU>("Terrain贴图烘焙(GPU)");
|
||
window.minSize = new Vector2(450, 750);
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
if (targetTerrain == null)
|
||
{
|
||
targetTerrain = FindObjectOfType<Terrain>();
|
||
}
|
||
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<Shader>(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<Terrain>();
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
}
|