NLDClient-yudde/ProjectNLD/Assets/Editor/Tools/TerrainBaker/TerrainBakerGPU.cs

1231 lines
44 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

using System;
using System.IO;
using UnityEditor;
using UnityEngine;
public class TerrainBakerGPU : EditorWindow
{
private Terrain targetTerrain;
private int textureResolution = 2048;
private bool bakeAlbedo = true;
private bool bakeNormal = true;
private bool bakeHeightmap = false;
private float normalStrength = 1.0f;
private bool createBakedQuad = true;
private string mapId = "";
private bool bakeWaterMask = true;
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(5);
bakeWaterMask = EditorGUILayout.Toggle("烘焙WaterMask (排除WaterMask_Layer)", bakeWaterMask);
GUILayout.Space(5);
createBakedQuad = EditorGUILayout.Toggle("生成场景Quad (带材质)", createBakedQuad);
EditorGUILayout.LabelField("命名规范", EditorStyles.boldLabel);
mapId = EditorGUILayout.TextField("Map编号", mapId);
EditorGUILayout.LabelField(" (例: 4_5_3_1,贴图=ColorMap_4_5_3_1.png,材质=Map_4_5_3_1.mat)", EditorStyles.miniLabel);
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 });
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 && !bakeWaterMask)
{
EditorUtility.DisplayDialog("错误", "请至少选择一种贴图类型进行烘焙!", "确定");
return;
}
try
{
string terrainName = targetTerrain.name;
string albedoPath = null;
int savedCount = 0;
string effectiveMapId = string.IsNullOrWhiteSpace(mapId) ? terrainName : mapId;
string colorMapName = $"ColorMap_{effectiveMapId}";
string matName = $"Map_{effectiveMapId}";
if (bakeAlbedo)
{
EditorUtility.DisplayProgressBar("烘焙进度", "正在烘焙漫反射贴图...", 0.2f);
Texture2D albedoTex = BakeAlbedoTexture(textureResolution);
albedoPath = $"Assets/BakedTerrainTextures/{colorMapName}.png";
SaveTexture(albedoTex, albedoPath);
DestroyImmediate(albedoTex);
savedCount++;
Debug.Log($"漫反射贴图已保存到: {albedoPath}");
}
if (bakeNormal)
{
EditorUtility.DisplayProgressBar("烘焙进度", "正在烘焙法线贴图...", 0.5f);
Texture2D normalTex = BakeNormalTexture(textureResolution);
string normalPath = $"Assets/BakedTerrainTextures/{colorMapName}_Normal.png";
SaveTexture(normalTex, normalPath, true);
DestroyImmediate(normalTex);
savedCount++;
Debug.Log($"法线贴图已保存到: {normalPath}");
}
if (bakeHeightmap)
{
EditorUtility.DisplayProgressBar("烘焙进度", "正在烘焙高度图...", 0.7f);
Texture2D heightTex = BakeHeightmapTexture(textureResolution);
string heightPath = $"Assets/BakedTerrainTextures/{colorMapName}_Height.png";
SaveTexture(heightTex, heightPath);
DestroyImmediate(heightTex);
savedCount++;
Debug.Log($"高度图已保存到: {heightPath}");
}
if (bakeWaterMask)
{
EditorUtility.DisplayProgressBar("烘焙进度", "正在烘焙WaterMask...", 0.85f);
Texture2D waterMaskTex = BakeWaterMaskTexture(textureResolution);
if (waterMaskTex != null)
{
string waterMaskPath = $"Assets/BakedTerrainTextures/{colorMapName}_WaterMask.png";
SaveTexture(waterMaskTex, waterMaskPath);
DestroyImmediate(waterMaskTex);
savedCount++;
Debug.Log($"WaterMask已保存到: {waterMaskPath}");
}
}
EditorUtility.ClearProgressBar();
AssetDatabase.Refresh();
if (createBakedQuad && (!string.IsNullOrEmpty(albedoPath) || bakeWaterMask))
{
CreateOrUpdateBakedQuad(albedoPath);
}
string outputFolder = "Assets/Art/Scene/Map/Textures/ColorMap";
EditorUtility.DisplayDialog("完成", $"成功烘焙并保存了 {savedCount} 张贴图到:\n{outputFolder}\n材质保存到:\nAssets/Art/Scene/Map/Mat", "确定");
}
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 CreateControlExcludingWaterLayer(Texture2D originalAlphaMap, int alphamapIndex, int waterLayerIndex)
{
if (waterLayerIndex < 0) return originalAlphaMap;
int startLayer = alphamapIndex * 4;
int channel = waterLayerIndex - startLayer;
if (channel < 0 || channel > 3) return originalAlphaMap;
Color[] pixels = originalAlphaMap.GetPixels();
for (int i = 0; i < pixels.Length; i++)
{
float removed = 0;
switch (channel)
{
case 0: removed = pixels[i].r; pixels[i].r = 0; break;
case 1: removed = pixels[i].g; pixels[i].g = 0; break;
case 2: removed = pixels[i].b; pixels[i].b = 0; break;
case 3: removed = pixels[i].a; pixels[i].a = 0; break;
}
float sum = pixels[i].r + pixels[i].g + pixels[i].b + pixels[i].a;
if (sum > 0.001f)
{
float scale = 1.0f / sum;
pixels[i].r *= scale;
pixels[i].g *= scale;
pixels[i].b *= scale;
pixels[i].a *= scale;
}
}
Texture2D result = new Texture2D(originalAlphaMap.width, originalAlphaMap.height, TextureFormat.RGBA32, false);
result.SetPixels(pixels);
result.Apply();
return result;
}
private Texture2D BakeAlbedoWithGPU(int resolution, TerrainData terrainData, TerrainLayer[] terrainLayers)
{
int layerCount = terrainLayers.Length;
int alphamapCount = terrainData.alphamapTextureCount;
Vector3 terrainSize = terrainData.size;
int waterMaskLayerIndex = GetWaterMaskLayerIndex(terrainLayers);
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);
Texture2D processedAlphaMap = CreateControlExcludingWaterLayer(alphaMap, alphamapIndex, waterMaskLayerIndex);
bakerMat.SetTexture("_Control", processedAlphaMap);
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 && layerIndex != waterMaskLayerIndex)
{
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);
}
if (processedAlphaMap != alphaMap)
{
DestroyImmediate(processedAlphaMap);
}
}
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 int GetWaterMaskLayerIndex(TerrainLayer[] terrainLayers)
{
for (int i = 0; i < terrainLayers.Length; i++)
{
if (terrainLayers[i] != null && terrainLayers[i].name == "WaterMask_Layer")
{
return i;
}
}
return -1;
}
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;
int waterMaskLayerIndex = GetWaterMaskLayerIndex(terrainLayers);
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;
float totalWeight = 0;
for (int layer = 0; layer < layerCount; layer++)
{
if (layer == waterMaskLayerIndex) continue;
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;
totalWeight += weight;
}
}
if (totalWeight > 0.001f && totalWeight < 0.999f)
{
finalColor /= totalWeight;
}
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 BakeWaterMaskTexture(int resolution)
{
TerrainData terrainData = targetTerrain.terrainData;
TerrainLayer[] terrainLayers = terrainData.terrainLayers;
int waterLayerIndex = GetWaterMaskLayerIndex(terrainLayers);
if (waterLayerIndex == -1)
{
Debug.LogWarning("Terrain 中未找到名为 'WaterMask_Layer' 的 Layer,跳过WaterMask烘焙");
return null;
}
float[,,] alphaMaps = terrainData.GetAlphamaps(0, 0, terrainData.alphamapWidth, terrainData.alphamapHeight);
int alphaWidth = terrainData.alphamapWidth;
int alphaHeight = terrainData.alphamapHeight;
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;
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);
float weight = alphaMaps[alphaY, alphaX, waterLayerIndex];
pixels[y * resolution + x] = new Color(weight, weight, weight, 1);
}
if (y % 100 == 0)
{
EditorUtility.DisplayProgressBar("烘焙进度", $"正在烘焙WaterMask... ({y}/{resolution})", (float)y / resolution * 0.3f + 0.85f);
}
}
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();
}
}
}
private void CreateOrUpdateBakedQuad(string albedoPath)
{
if (targetTerrain == null) return;
TerrainData terrainData = targetTerrain.terrainData;
Vector3 terrainSize = terrainData.size;
Vector3 terrainPos = targetTerrain.transform.position;
string quadName = targetTerrain.name + "_BakedQuad";
GameObject quad = GameObject.Find(quadName);
if (quad == null)
{
quad = GameObject.CreatePrimitive(PrimitiveType.Quad);
quad.name = quadName;
DestroyImmediate(quad.GetComponent<Collider>());
}
quad.transform.position = new Vector3(
terrainPos.x + terrainSize.x * 0.5f,
terrainPos.y + 0.01f,
terrainPos.z + terrainSize.z * 0.5f
);
quad.transform.localScale = new Vector3(terrainSize.x, terrainSize.z, 1f);
quad.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
string sampleMatPath = "Assets/Editor/Tools/TerrainBaker/TerrainSample.mat";
Material sampleMat = AssetDatabase.LoadAssetAtPath<Material>(sampleMatPath);
if (sampleMat == null)
{
Debug.LogError($"无法加载样本材质: {sampleMatPath}");
return;
}
string matFolder = "Assets/Art/Scene/Map/Mat";
string texFolder = "Assets/Art/Scene/Map/Textures/ColorMap";
if (!Directory.Exists(matFolder))
{
Directory.CreateDirectory(matFolder);
}
if (!Directory.Exists(texFolder))
{
Directory.CreateDirectory(texFolder);
}
string effectiveMapId = string.IsNullOrWhiteSpace(mapId) ? targetTerrain.name : mapId;
string matName = $"Map_{effectiveMapId}";
string matPath = matFolder.Replace("\\", "/") + "/" + matName + ".mat";
string albedoAssetPath = albedoPath.Replace("\\", "/");
Texture2D albedoTex = AssetDatabase.LoadAssetAtPath<Texture2D>(albedoAssetPath);
Material bakedMat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
if (bakedMat != null)
{
bakedMat.CopyPropertiesFromMaterial(sampleMat);
}
else
{
bakedMat = new Material(sampleMat);
AssetDatabase.CreateAsset(bakedMat, matPath);
}
if (albedoTex != null)
{
bakedMat.SetTexture("_BaseMap", albedoTex);
bakedMat.SetTexture("_MainTex", albedoTex);
}
// 尝试通过文件名加载WaterMask
string waterMaskPath = $"Assets/BakedTerrainTextures/ColorMap_{effectiveMapId}_WaterMask.png";
Texture2D waterMaskTex = AssetDatabase.LoadAssetAtPath<Texture2D>(waterMaskPath);
if (waterMaskTex == null)
{
string absolutePath = Path.Combine(Path.GetDirectoryName(Application.dataPath), waterMaskPath);
if (File.Exists(absolutePath))
{
AssetDatabase.ImportAsset(waterMaskPath, ImportAssetOptions.ForceSynchronousImport);
waterMaskTex = AssetDatabase.LoadAssetAtPath<Texture2D>(waterMaskPath);
}
}
// 如果文件名匹配失败,通过GUID兜底查找BakedTerrainTextures目录下所有WaterMask贴图
if (waterMaskTex == null)
{
string[] guids = AssetDatabase.FindAssets("t:Texture2D", new[] { "Assets/BakedTerrainTextures" });
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
if (path.Contains("_WaterMask"))
{
waterMaskTex = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
if (waterMaskTex != null)
{
waterMaskPath = path;
break;
}
}
}
}
if (waterMaskTex != null)
{
bakedMat.SetTexture("_WaterMask", waterMaskTex);
bakedMat.SetFloat("_UseWaterMask", 1f);
Debug.Log($"材质已启用WaterMask: {waterMaskPath}");
}
else
{
bakedMat.SetFloat("_UseWaterMask", 0f);
Debug.LogWarning($"未找到WaterMask贴图,材质回退到亮度检测模式: {waterMaskPath}");
}
EditorUtility.SetDirty(bakedMat);
AssetDatabase.SaveAssets();
MeshRenderer renderer = quad.GetComponent<MeshRenderer>();
if (renderer != null)
{
renderer.sharedMaterial = bakedMat;
}
Selection.activeGameObject = quad;
Debug.Log($"已生成/更新烘焙Quad: {quad.name}, 材质: {matPath}");
}
}