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

982 lines
34 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 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();
}
}
}
}