Terrain Baker 工具
parent
b02ba5fd00
commit
8c359f3e15
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c8483871456c1094fab807a56c97ddd2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e768000910317504396dc437fb84069e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 1df76e7b121cd56448c2ca47212d6c8a
|
||||
ShaderImporter:
|
||||
externalObjects: {}
|
||||
defaultTextures: []
|
||||
nonModifiableTextures: []
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Reference in New Issue