Terrain Baker 工具

main
Vinny 2026-01-05 17:38:14 +08:00 committed by 马远征
parent b02ba5fd00
commit 8c359f3e15
5 changed files with 1105 additions and 0 deletions

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c8483871456c1094fab807a56c97ddd2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,981 @@
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();
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e768000910317504396dc437fb84069e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,96 @@
Shader "Hidden/TerrainBaker/AlbedoBaker"
{
Properties
{
_MainTex ("Main Tex", 2D) = "white" {}
_Control ("Control (Splatmap)", 2D) = "black" {}
_Splat0 ("Layer 0", 2D) = "white" {}
_Splat1 ("Layer 1", 2D) = "white" {}
_Splat2 ("Layer 2", 2D) = "white" {}
_Splat3 ("Layer 3", 2D) = "white" {}
_TerrainSize ("Terrain Size", Vector) = (100,100,0,0)
_TileSize0 ("Tile Size 0", Vector) = (10,10,0,0)
_TileSize1 ("Tile Size 1", Vector) = (10,10,0,0)
_TileSize2 ("Tile Size 2", Vector) = (10,10,0,0)
_TileSize3 ("Tile Size 3", Vector) = (10,10,0,0)
_GammaCorrection ("Gamma Correction", Float) = 1.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Cull Off
ZWrite Off
ZTest Always
Pass
{
Name "BakeAlbedo"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 3.0
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
sampler2D _Control;
sampler2D _Splat0, _Splat1, _Splat2, _Splat3;
float4 _TerrainSize;
float4 _TileSize0, _TileSize1, _TileSize2, _TileSize3;
float _GammaCorrection;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float4 control = tex2D(_Control, i.uv);
float2 worldXZ = i.uv * _TerrainSize.xy;
float2 uv0 = worldXZ / _TileSize0.xy + _TileSize0.zw;
float2 uv1 = worldXZ / _TileSize1.xy + _TileSize1.zw;
float2 uv2 = worldXZ / _TileSize2.xy + _TileSize2.zw;
float2 uv3 = worldXZ / _TileSize3.xy + _TileSize3.zw;
fixed4 splat0 = tex2D(_Splat0, uv0);
fixed4 splat1 = tex2D(_Splat1, uv1);
fixed4 splat2 = tex2D(_Splat2, uv2);
fixed4 splat3 = tex2D(_Splat3, uv3);
fixed4 result = splat0 * control.r
+ splat1 * control.g
+ splat2 * control.b
+ splat3 * control.a;
result.rgb = pow(result.rgb, _GammaCorrection);
result.a = 1;
return result;
}
ENDCG
}
}
FallBack Off
}

View File

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 1df76e7b121cd56448c2ca47212d6c8a
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant: