#添加场景替换工具

This commit is contained in:
ZombieKitty 2025-06-16 18:43:39 +08:00
parent c71d21b73b
commit 24a8da25dc
5 changed files with 971 additions and 1 deletions

View File

@ -9,7 +9,7 @@ namespace MindPowerSdk.Editor
/// 地图生成工具窗口,用于配置地图数据参数、材质映射,以及生成、清除、保存地图到预制体。 /// 地图生成工具窗口,用于配置地图数据参数、材质映射,以及生成、清除、保存地图到预制体。
/// 注意:每个地图配置项对应一个 MapName 与资源数据名称组合生成一个地图。 /// 注意:每个地图配置项对应一个 MapName 与资源数据名称组合生成一个地图。
/// </summary> /// </summary>
public class MapGeneratorEditorWindow : EditorWindow public class MapGeneratorEditorWindow : UnityEditor.EditorWindow
{ {
// 文件夹路径设置 // 文件夹路径设置
private string mapDataFolder = ""; private string mapDataFolder = "";

View File

@ -0,0 +1,587 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
using Object = UnityEngine.Object;
namespace MindPowerSdk.EditorWindow
{
/// <summary>
/// Unity对象替换工具
/// 用于搜索并替换场景中包含指定名称的游戏对象
/// </summary>
public class ObjectReplacerWindow : UnityEditor.EditorWindow
{
[Serializable]
public class ReplacementConfig
{
[Header("搜索设置")]
public string searchName = "";
public bool useExactMatch = false;
[Header("替换设置")]
public GameObject replacementPrefab;
[Header("保留选项")]
public bool preservePosition = true;
public bool preserveRotation = true;
public bool preserveScale = true;
public bool preserveComponents = false;
[Header("组件过滤")]
public List<string> componentsToPreserve = new List<string>();
}
#region
private ReplacementConfig config = new ReplacementConfig();
private Vector2 scrollPosition;
private List<GameObject> foundObjects = new List<GameObject>();
private bool showFoundObjects = false;
private bool showComponentOptions = false;
// 配置管理
private string configName = "新配置";
private List<ReplacementConfig> savedConfigs = new List<ReplacementConfig>();
private int selectedConfigIndex = 0;
private string[] configNames = new string[0];
#endregion
#region Unity生命周期
[MenuItem("MindPowerSdk/工具/对象替换器")]
public static void ShowWindow()
{
var window = GetWindow<ObjectReplacerWindow>("对象替换工具");
window.minSize = new Vector2(420, 600);
window.Show();
}
private void OnEnable()
{
LoadConfigs();
UpdateConfigNames();
}
private void OnDisable()
{
SaveConfigs();
}
#endregion
#region GUI绘制
private void OnGUI()
{
EditorGUILayout.BeginVertical();
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
DrawHeader();
DrawSearchSection();
DrawReplacementSection();
DrawOptionsSection();
DrawActionSection();
DrawResultsSection();
DrawConfigSection();
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
private void DrawHeader()
{
EditorGUILayout.Space(5);
var headerStyle = new GUIStyle(EditorStyles.largeLabel)
{
fontSize = 18,
fontStyle = FontStyle.Bold,
alignment = TextAnchor.MiddleCenter
};
EditorGUILayout.LabelField("🔄 Unity对象替换工具", headerStyle);
EditorGUILayout.LabelField("快速替换场景中的游戏对象", EditorStyles.centeredGreyMiniLabel);
EditorGUILayout.Space(10);
}
private void DrawSearchSection()
{
EditorGUILayout.LabelField("🔍 搜索设置", EditorStyles.boldLabel);
EditorGUILayout.BeginVertical("box");
config.searchName = EditorGUILayout.TextField("搜索名称", config.searchName);
config.useExactMatch = EditorGUILayout.Toggle("精确匹配", config.useExactMatch);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("🔍 搜索对象"))
{
SearchObjects();
}
if (GUILayout.Button("🧹 清空结果"))
{
ClearResults();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
}
private void DrawReplacementSection()
{
EditorGUILayout.LabelField("🎯 替换设置", EditorStyles.boldLabel);
EditorGUILayout.BeginVertical("box");
config.replacementPrefab = (GameObject)EditorGUILayout.ObjectField(
"替换预制体", config.replacementPrefab, typeof(GameObject), false);
if (config.replacementPrefab == null)
{
EditorGUILayout.HelpBox("请选择用于替换的预制体或模型", MessageType.Warning);
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
}
private void DrawOptionsSection()
{
EditorGUILayout.LabelField("⚙️ 保留选项", EditorStyles.boldLabel);
EditorGUILayout.BeginVertical("box");
config.preservePosition = EditorGUILayout.Toggle("保留位置", config.preservePosition);
config.preserveRotation = EditorGUILayout.Toggle("保留旋转", config.preserveRotation);
config.preserveScale = EditorGUILayout.Toggle("保留缩放", config.preserveScale);
config.preserveComponents = EditorGUILayout.Toggle("保留组件", config.preserveComponents);
if (config.preserveComponents)
{
EditorGUILayout.Space(3);
showComponentOptions = EditorGUILayout.Foldout(showComponentOptions, "组件类型过滤");
if (showComponentOptions)
{
DrawComponentOptions();
}
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
}
private void DrawComponentOptions()
{
EditorGUILayout.BeginVertical("helpbox");
EditorGUILayout.LabelField("要保留的组件类型名称 (为空时保留所有组件):", EditorStyles.miniLabel);
for (int i = 0; i < config.componentsToPreserve.Count; i++)
{
EditorGUILayout.BeginHorizontal();
config.componentsToPreserve[i] = EditorGUILayout.TextField(config.componentsToPreserve[i]);
if (GUILayout.Button("✖", GUILayout.Width(25)))
{
config.componentsToPreserve.RemoveAt(i);
i--;
}
EditorGUILayout.EndHorizontal();
}
if (GUILayout.Button(" 添加组件类型"))
{
config.componentsToPreserve.Add("");
}
EditorGUILayout.EndVertical();
}
private void DrawActionSection()
{
EditorGUILayout.LabelField("🚀 执行操作", EditorStyles.boldLabel);
EditorGUILayout.BeginVertical("box");
bool canReplace = foundObjects.Count > 0 && config.replacementPrefab != null;
EditorGUI.BeginDisabledGroup(!canReplace);
var buttonStyle = new GUIStyle(GUI.skin.button) { fixedHeight = 35 };
if (GUILayout.Button($"🔄 替换 {foundObjects.Count} 个对象", buttonStyle))
{
if (ShowReplaceConfirmation())
{
ReplaceObjects();
}
}
EditorGUI.EndDisabledGroup();
if (!canReplace)
{
string message = foundObjects.Count == 0 ? "请先搜索要替换的对象" : "请选择替换用的预制体";
EditorGUILayout.HelpBox(message, MessageType.Info);
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
}
private void DrawResultsSection()
{
if (foundObjects.Count > 0)
{
showFoundObjects = EditorGUILayout.Foldout(showFoundObjects,
$"📋 搜索结果 ({foundObjects.Count} 个对象)");
if (showFoundObjects)
{
EditorGUILayout.BeginVertical("box");
int displayCount = Mathf.Min(foundObjects.Count, 15);
for (int i = 0; i < displayCount; i++)
{
var obj = foundObjects[i];
if (obj == null) continue;
EditorGUILayout.BeginHorizontal();
EditorGUILayout.ObjectField(obj, typeof(GameObject), true);
if (GUILayout.Button("定位", GUILayout.Width(40)))
{
Selection.activeGameObject = obj;
EditorGUIUtility.PingObject(obj);
}
EditorGUILayout.EndHorizontal();
}
if (foundObjects.Count > displayCount)
{
EditorGUILayout.HelpBox($"显示前 {displayCount} 个对象,总共 {foundObjects.Count} 个",
MessageType.Info);
}
EditorGUILayout.EndVertical();
}
EditorGUILayout.Space(5);
}
}
private void DrawConfigSection()
{
EditorGUILayout.LabelField("💾 配置管理", EditorStyles.boldLabel);
EditorGUILayout.BeginVertical("box");
// 保存配置
EditorGUILayout.BeginHorizontal();
configName = EditorGUILayout.TextField("配置名称", configName);
if (GUILayout.Button("保存", GUILayout.Width(50)))
{
SaveCurrentConfig();
}
EditorGUILayout.EndHorizontal();
// 加载配置
if (savedConfigs.Count > 0)
{
EditorGUILayout.BeginHorizontal();
selectedConfigIndex = EditorGUILayout.Popup("已保存配置", selectedConfigIndex, configNames);
if (GUILayout.Button("加载", GUILayout.Width(50)))
{
LoadConfig(selectedConfigIndex);
}
if (GUILayout.Button("删除", GUILayout.Width(50)))
{
DeleteConfig(selectedConfigIndex);
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
}
#endregion
#region
private void SearchObjects()
{
foundObjects.Clear();
if (string.IsNullOrEmpty(config.searchName.Trim()))
{
EditorUtility.DisplayDialog("输入错误", "请输入要搜索的对象名称!", "确定");
return;
}
var allObjects = FindObjectsOfType<GameObject>();
string searchTerm = config.searchName.Trim();
foreach (var obj in allObjects)
{
bool isMatch = config.useExactMatch
? obj.name.Equals(searchTerm, StringComparison.OrdinalIgnoreCase)
: obj.name.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0;
if (isMatch)
{
foundObjects.Add(obj);
}
}
showFoundObjects = foundObjects.Count > 0;
Debug.Log($"搜索完成: 找到 {foundObjects.Count} 个匹配的对象");
if (foundObjects.Count == 0)
{
EditorUtility.DisplayDialog("搜索结果", "未找到匹配的对象", "确定");
}
}
private void ClearResults()
{
foundObjects.Clear();
showFoundObjects = false;
Debug.Log("已清空搜索结果");
}
private bool ShowReplaceConfirmation()
{
return EditorUtility.DisplayDialog("确认替换",
$"确定要替换 {foundObjects.Count} 个对象吗?\n\n⚠ 此操作支持撤销(Ctrl+Z)",
"确认替换", "取消");
}
private void ReplaceObjects()
{
if (config.replacementPrefab == null || foundObjects.Count == 0) return;
Undo.SetCurrentGroupName("批量替换对象");
int undoGroup = Undo.GetCurrentGroup();
int successCount = 0;
var objectsToReplace = foundObjects.ToArray(); // 复制数组避免迭代中修改
foreach (var originalObj in objectsToReplace)
{
if (originalObj == null) continue;
try
{
if (ReplaceObject(originalObj))
{
successCount++;
}
}
catch (Exception e)
{
Debug.LogError($"替换对象 '{originalObj.name}' 时出错: {e.Message}");
}
}
Undo.CollapseUndoOperations(undoGroup);
Debug.Log($"替换完成: 成功替换 {successCount}/{foundObjects.Count} 个对象");
ClearResults();
EditorUtility.DisplayDialog("替换完成",
$"成功替换 {successCount} 个对象\n\n💡 可使用 Ctrl+Z 撤销操作", "确定");
}
private bool ReplaceObject(GameObject original)
{
// 记录原始变换信息
Transform originalTransform = original.transform;
Vector3 position = originalTransform.position;
Quaternion rotation = originalTransform.rotation;
Vector3 scale = originalTransform.localScale;
Transform parent = originalTransform.parent;
int siblingIndex = originalTransform.GetSiblingIndex();
// 获取要保留的组件
Component[] componentsToKeep = null;
if (config.preserveComponents)
{
componentsToKeep = GetComponentsToPreserve(original);
}
// 创建新对象
GameObject newObj = PrefabUtility.InstantiatePrefab(config.replacementPrefab) as GameObject;
if (newObj == null)
{
newObj = Instantiate(config.replacementPrefab);
}
// 注册撤销操作
Undo.RegisterCreatedObjectUndo(newObj, "创建替换对象");
// 设置层级和位置
newObj.transform.SetParent(parent);
newObj.transform.SetSiblingIndex(siblingIndex);
// 应用变换
if (config.preservePosition) newObj.transform.position = position;
if (config.preserveRotation) newObj.transform.rotation = rotation;
if (config.preserveScale) newObj.transform.localScale = scale;
// 复制组件
if (config.preserveComponents && componentsToKeep != null)
{
CopyComponents(componentsToKeep, newObj);
}
// 删除原对象
Undo.DestroyObjectImmediate(original);
return true;
}
private Component[] GetComponentsToPreserve(GameObject obj)
{
var allComponents = obj.GetComponents<Component>();
if (config.componentsToPreserve.Count == 0)
{
// 保留所有组件 (除了Transform)
return allComponents.Where(c => c != null && !(c is Transform)).ToArray();
}
// 只保留指定类型的组件
var components = new List<Component>();
foreach (var component in allComponents)
{
if (component == null || component is Transform) continue;
string componentTypeName = component.GetType().Name;
bool shouldPreserve = config.componentsToPreserve.Any(typeName =>
!string.IsNullOrEmpty(typeName) &&
componentTypeName.IndexOf(typeName.Trim(), StringComparison.OrdinalIgnoreCase) >= 0);
if (shouldPreserve)
{
components.Add(component);
}
}
return components.ToArray();
}
private void CopyComponents(Component[] components, GameObject target)
{
foreach (var component in components)
{
if (component == null) continue;
try
{
UnityEditorInternal.ComponentUtility.CopyComponent(component);
UnityEditorInternal.ComponentUtility.PasteComponentAsNew(target);
}
catch (Exception e)
{
Debug.LogWarning($"复制组件 {component.GetType().Name} 失败: {e.Message}");
}
}
}
#endregion
#region
private void SaveCurrentConfig()
{
if (string.IsNullOrEmpty(configName.Trim()))
{
EditorUtility.DisplayDialog("保存失败", "请输入配置名称!", "确定");
return;
}
var newConfig = JsonUtility.FromJson<ReplacementConfig>(JsonUtility.ToJson(config));
// 检查是否已存在
int existingIndex = savedConfigs.FindIndex(c => c.searchName == configName);
if (existingIndex >= 0)
{
if (EditorUtility.DisplayDialog("配置已存在", "是否覆盖现有配置?", "覆盖", "取消"))
{
savedConfigs[existingIndex] = newConfig;
savedConfigs[existingIndex].searchName = configName;
}
else
{
return;
}
}
else
{
newConfig.searchName = configName;
savedConfigs.Add(newConfig);
}
SaveConfigs();
UpdateConfigNames();
Debug.Log($"配置 '{configName}' 已保存");
}
private void LoadConfig(int index)
{
if (index >= 0 && index < savedConfigs.Count)
{
var configToLoad = savedConfigs[index];
config = JsonUtility.FromJson<ReplacementConfig>(JsonUtility.ToJson(configToLoad));
configName = configToLoad.searchName;
Debug.Log($"配置 '{configToLoad.searchName}' 已加载");
}
}
private void DeleteConfig(int index)
{
if (index >= 0 && index < savedConfigs.Count)
{
string configToDelete = savedConfigs[index].searchName;
if (EditorUtility.DisplayDialog("确认删除", $"确定要删除配置 '{configToDelete}' 吗?", "删除", "取消"))
{
savedConfigs.RemoveAt(index);
selectedConfigIndex = Mathf.Max(0, selectedConfigIndex - 1);
SaveConfigs();
UpdateConfigNames();
Debug.Log($"配置 '{configToDelete}' 已删除");
}
}
}
private void UpdateConfigNames()
{
configNames = savedConfigs.Select(c => c.searchName ?? "未命名").ToArray();
selectedConfigIndex = Mathf.Clamp(selectedConfigIndex, 0, Mathf.Max(0, configNames.Length - 1));
}
private void SaveConfigs()
{
try
{
string json = JsonUtility.ToJson(new ConfigList { configs = savedConfigs }, true);
EditorPrefs.SetString("ObjectReplacer_SavedConfigs", json);
}
catch (Exception e)
{
Debug.LogError($"保存配置失败: {e.Message}");
}
}
private void LoadConfigs()
{
try
{
string json = EditorPrefs.GetString("ObjectReplacer_SavedConfigs", "");
if (!string.IsNullOrEmpty(json))
{
var configList = JsonUtility.FromJson<ConfigList>(json);
savedConfigs = configList?.configs ?? new List<ReplacementConfig>();
}
else
{
savedConfigs = new List<ReplacementConfig>();
}
}
catch (Exception e)
{
Debug.LogWarning($"加载配置失败: {e.Message}");
savedConfigs = new List<ReplacementConfig>();
}
}
[Serializable]
private class ConfigList
{
public List<ReplacementConfig> configs;
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,369 @@
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections.Generic;
public class TextureMergerWindow : EditorWindow
{
// 可选的目标合并贴图分辨率选项
public enum MergeResolution
{
_1024 = 1024,
_2048 = 2048,
_4096 = 4096,
_8192 = 8192
}
private TextAsset terrainInfo; // 记录贴图信息的 TextAsset例如 TerrainInfo.txt
private MergeResolution selectedResolution = MergeResolution._1024;
// 自定义行列设置,默认不启用
private bool useCustomGrid = false;
private int customGridCount = 8; // 自定义行列数(行数=列数例如8行8列
// 附加贴图设置(法线、金属度、平滑度、高度)
private bool _isMergeNormalMaps;
private string _mergeNormalNameSuffix = "_normal"; // 法线贴图后缀
private bool _isMergeMetallicMaps;
private string _mergeMetallicMapSuffix = "_metallic"; // 金属度贴图后缀
private bool _isMergeSmoothnessMaps;
private string _mergeSmoothnessMapNameSuffix = "_smoothness"; // 平滑度贴图后缀
private bool _isMergeHeightMaps;
private string _mergeHeightMapNameSuffix = "_height"; // 高度贴图后缀
// 保存路径
private string saveFolder = Application.dataPath;
[MenuItem("Window/Texture Merger")]
public static void ShowWindow()
{
GetWindow<TextureMergerWindow>("Texture Merger");
}
private void OnGUI()
{
GUILayout.Label("贴图合并设置", EditorStyles.boldLabel);
terrainInfo =
(TextAsset)EditorGUILayout.ObjectField("Terrain Info (TextAsset)", terrainInfo, typeof(TextAsset), false);
selectedResolution = (MergeResolution)EditorGUILayout.EnumPopup("合并后贴图分辨率", selectedResolution);
useCustomGrid = EditorGUILayout.Toggle("使用自定义行列", useCustomGrid);
if (useCustomGrid)
{
customGridCount = EditorGUILayout.IntField("网格数量 (行=列)", customGridCount);
if (customGridCount < 1)
customGridCount = 1;
}
// 附加贴图设置
GUILayout.Space(10);
GUILayout.Label("附加贴图设置", EditorStyles.boldLabel);
_isMergeNormalMaps = EditorGUILayout.Toggle("合并法线贴图", _isMergeNormalMaps);
if (_isMergeNormalMaps)
_mergeNormalNameSuffix = EditorGUILayout.TextField("法线贴图后缀", _mergeNormalNameSuffix);
_isMergeMetallicMaps = EditorGUILayout.Toggle("合并金属度贴图", _isMergeMetallicMaps);
if (_isMergeMetallicMaps)
_mergeMetallicMapSuffix = EditorGUILayout.TextField("金属度贴图后缀", _mergeMetallicMapSuffix);
_isMergeSmoothnessMaps = EditorGUILayout.Toggle("合并平滑度贴图", _isMergeSmoothnessMaps);
if (_isMergeSmoothnessMaps)
_mergeSmoothnessMapNameSuffix = EditorGUILayout.TextField("平滑度贴图后缀", _mergeSmoothnessMapNameSuffix);
_isMergeHeightMaps = EditorGUILayout.Toggle("合并高度贴图", _isMergeHeightMaps);
if (_isMergeHeightMaps)
_mergeHeightMapNameSuffix = EditorGUILayout.TextField("高度贴图后缀", _mergeHeightMapNameSuffix);
// 保存路径选择
GUILayout.Space(10);
GUILayout.Label("保存路径设置", EditorStyles.boldLabel);
if (GUILayout.Button("选择保存路径"))
{
string folder = EditorUtility.OpenFolderPanel("选择保存路径", Application.dataPath, "");
if (!string.IsNullOrEmpty(folder))
{
saveFolder = folder;
}
}
EditorGUILayout.LabelField("当前保存路径:", string.IsNullOrEmpty(saveFolder) ? "未选择" : saveFolder);
GUILayout.Space(10);
if (GUILayout.Button("合并贴图"))
{
if (terrainInfo == null)
{
EditorUtility.DisplayDialog("错误", "请先指定包含贴图信息的 TextAsset", "确定");
return;
}
MergeTextures(terrainInfo, (int)selectedResolution);
}
}
/// <summary>
/// 根据 terrainInfo 中的信息加载基础贴图及附加贴图,并分别合并到目标分辨率的贴图上
/// </summary>
private void MergeTextures(TextAsset terrainInfo, int targetResolution)
{
// 准备各贴图列表
List<Texture2D> baseTextures = new List<Texture2D>();
List<Texture2D> normalTextures = new List<Texture2D>();
List<Texture2D> metallicTextures = new List<Texture2D>();
List<Texture2D> smoothnessTextures = new List<Texture2D>();
List<Texture2D> heightTextures = new List<Texture2D>();
string[] lines = terrainInfo.text.Split(new char[] { '\n' }, System.StringSplitOptions.RemoveEmptyEntries);
foreach (string line in lines)
{
string trimmedLine = line.Trim();
if (string.IsNullOrEmpty(trimmedLine))
continue;
// 每行要求格式为:其他信息\t贴图名称
string[] parts = trimmedLine.Split(new char[] { '\t' }, System.StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2)
continue;
string texName = Path.GetFileNameWithoutExtension(parts[1]);
// 加载基础贴图
string basePath = "Textures/Terrain/" + texName;
Texture2D baseTex = Resources.Load<Texture2D>(basePath);
if (baseTex != null)
{
baseTextures.Add(baseTex);
}
else
{
Debug.LogWarning("在路径 " + basePath + " 找不到基础贴图");
}
// 加载法线贴图(必须存在,否则提示错误)
if (_isMergeNormalMaps)
{
string normalPath = "Textures/Terrain/" + texName + _mergeNormalNameSuffix;
Texture2D normalTex = Resources.Load<Texture2D>(normalPath);
if (normalTex != null)
{
normalTextures.Add(normalTex);
}
else
{
EditorUtility.DisplayDialog("错误", "法线贴图缺失: " + normalPath, "确定");
return;
}
}
// 加载金属度贴图
if (_isMergeMetallicMaps)
{
string metallicPath = "Textures/Terrain/" + texName + _mergeMetallicMapSuffix;
Texture2D metallicTex = Resources.Load<Texture2D>(metallicPath);
if (metallicTex != null)
{
metallicTextures.Add(metallicTex);
}
else
{
Debug.LogWarning("在路径 " + metallicPath + " 找不到金属度贴图");
}
}
// 加载平滑度贴图
if (_isMergeSmoothnessMaps)
{
string smoothnessPath = "Textures/Terrain/" + texName + _mergeSmoothnessMapNameSuffix;
Texture2D smoothnessTex = Resources.Load<Texture2D>(smoothnessPath);
if (smoothnessTex != null)
{
smoothnessTextures.Add(smoothnessTex);
}
else
{
Debug.LogWarning("在路径 " + smoothnessPath + " 找不到平滑度贴图");
}
}
// 加载高度贴图
if (_isMergeHeightMaps)
{
string heightPath = "Textures/Terrain/" + texName + _mergeHeightMapNameSuffix;
Texture2D heightTex = Resources.Load<Texture2D>(heightPath);
if (heightTex != null)
{
heightTextures.Add(heightTex);
}
else
{
Debug.LogWarning("在路径 " + heightPath + " 找不到高度贴图");
}
}
}
if (baseTextures.Count == 0)
{
Debug.LogError("没有找到可合并的基础贴图!");
return;
}
// 合并基础贴图
Texture2D mergedBase = MergeTextureList(baseTextures, targetResolution);
SaveTexture(mergedBase,
Path.Combine(string.IsNullOrEmpty(saveFolder) ? Application.dataPath : saveFolder,
"MergedTexture.png"));
// 合并法线贴图
if (_isMergeNormalMaps)
{
if (normalTextures.Count != baseTextures.Count)
{
EditorUtility.DisplayDialog("错误", "法线贴图数量与基础贴图数量不匹配!", "确定");
return;
}
Texture2D mergedNormal = MergeTextureList(normalTextures, targetResolution);
SaveTexture(mergedNormal,
Path.Combine(string.IsNullOrEmpty(saveFolder) ? Application.dataPath : saveFolder,
"MergedTexture" + _mergeNormalNameSuffix + ".png"));
}
// 合并金属度贴图
if (_isMergeMetallicMaps && metallicTextures.Count > 0)
{
Texture2D mergedMetallic = MergeTextureList(metallicTextures, targetResolution);
SaveTexture(mergedMetallic,
Path.Combine(string.IsNullOrEmpty(saveFolder) ? Application.dataPath : saveFolder,
"MergedTexture" + _mergeMetallicMapSuffix + ".png"));
}
// 合并平滑度贴图
if (_isMergeSmoothnessMaps && smoothnessTextures.Count > 0)
{
Texture2D mergedSmoothness = MergeTextureList(smoothnessTextures, targetResolution);
SaveTexture(mergedSmoothness,
Path.Combine(string.IsNullOrEmpty(saveFolder) ? Application.dataPath : saveFolder,
"MergedTexture" + _mergeSmoothnessMapNameSuffix + ".png"));
}
// 合并高度贴图
if (_isMergeHeightMaps && heightTextures.Count > 0)
{
Texture2D mergedHeight = MergeTextureList(heightTextures, targetResolution);
SaveTexture(mergedHeight,
Path.Combine(string.IsNullOrEmpty(saveFolder) ? Application.dataPath : saveFolder,
"MergedTexture" + _mergeHeightMapNameSuffix + ".png"));
}
}
/// <summary>
/// 将传入的贴图列表按照设定的网格规则合并到一张贴图中
/// </summary>
private Texture2D MergeTextureList(List<Texture2D> textureList, int targetResolution)
{
int gridCols, gridRows, tileSize, intermediateWidth, intermediateHeight;
if (useCustomGrid)
{
gridCols = customGridCount;
gridRows = customGridCount;
int maxTiles = gridCols * gridRows;
if (textureList.Count > maxTiles)
{
Debug.LogWarning("贴图数量超过设定的网格数(" + maxTiles + "),将只合并前 " + maxTiles + " 个贴图。");
textureList = textureList.GetRange(0, maxTiles);
}
tileSize = targetResolution / gridCols;
intermediateWidth = targetResolution;
intermediateHeight = targetResolution;
}
else
{
int count = textureList.Count;
gridCols = Mathf.CeilToInt(Mathf.Sqrt(count));
gridRows = Mathf.CeilToInt((float)count / gridCols);
tileSize = 512;
intermediateWidth = gridCols * tileSize;
intermediateHeight = gridRows * tileSize;
}
// 创建中间纹理,填充黑色背景
Texture2D intermediateTexture =
new Texture2D(intermediateWidth, intermediateHeight, TextureFormat.RGBA32, false);
Color[] fillColor = new Color[intermediateWidth * intermediateHeight];
for (int i = 0; i < fillColor.Length; i++)
fillColor[i] = Color.black;
intermediateTexture.SetPixels(fillColor);
// 将每张贴图缩放后依次放入中间纹理
for (int i = 0; i < textureList.Count; i++)
{
Texture2D srcTex = textureList[i];
Texture2D resizedTex = ScaleTexture(srcTex, tileSize, tileSize);
int col = i % gridCols;
int row = i / gridCols;
int startX = col * tileSize;
int startY = (gridRows - 1 - row) * tileSize;
intermediateTexture.SetPixels(startX, startY, tileSize, tileSize, resizedTex.GetPixels());
}
intermediateTexture.Apply();
// 最终贴图:初始化为目标分辨率,并将中间纹理缩放后居中放置
Texture2D finalTexture = new Texture2D(targetResolution, targetResolution, TextureFormat.RGBA32, false);
Color[] finalFill = new Color[targetResolution * targetResolution];
for (int i = 0; i < finalFill.Length; i++)
finalFill[i] = Color.black;
finalTexture.SetPixels(finalFill);
float scaleFactor = Mathf.Min((float)targetResolution / intermediateWidth,
(float)targetResolution / intermediateHeight);
int scaledWidth = Mathf.RoundToInt(intermediateWidth * scaleFactor);
int scaledHeight = Mathf.RoundToInt(intermediateHeight * scaleFactor);
Texture2D scaledIntermediate = ScaleTexture(intermediateTexture, scaledWidth, scaledHeight);
int offsetX = (targetResolution - scaledWidth) / 2;
int offsetY = (targetResolution - scaledHeight) / 2;
finalTexture.SetPixels(offsetX, offsetY, scaledWidth, scaledHeight, scaledIntermediate.GetPixels());
finalTexture.Apply();
return finalTexture;
}
/// <summary>
/// 使用 RenderTexture 和 Graphics.Blit 进行高性能的贴图缩放
/// 1. 结果贴图统一使用 RGBA32 格式
/// 2. Blit 后调用 GL.Flush() 确保渲染命令执行完毕
/// 3. 保存并恢复之前的 RenderTexture.active
/// </summary>
private Texture2D ScaleTexture(Texture2D source, int targetWidth, int targetHeight)
{
RenderTexture rt = RenderTexture.GetTemporary(targetWidth, targetHeight);
rt.filterMode = FilterMode.Bilinear;
RenderTexture previous = RenderTexture.active;
RenderTexture.active = rt;
Graphics.Blit(source, rt);
GL.Flush();
Texture2D result = new Texture2D(targetWidth, targetHeight, TextureFormat.RGBA32, false);
result.ReadPixels(new Rect(0, 0, targetWidth, targetHeight), 0, 0);
result.Apply();
RenderTexture.active = previous;
RenderTexture.ReleaseTemporary(rt);
return result;
}
/// <summary>
/// 将生成的贴图保存为 PNG 文件,并刷新 AssetDatabase
/// </summary>
private void SaveTexture(Texture2D texture, string path)
{
byte[] pngData = texture.EncodeToPNG();
File.WriteAllBytes(path, pngData);
Debug.Log("合并贴图已保存至: " + path);
AssetDatabase.Refresh();
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 523dc48301cf4088a8eeec5486352117
timeCreated: 1743696612