537 lines
18 KiB
C#
537 lines
18 KiB
C#
using UnityEngine;
|
||
using UnityEditor;
|
||
using System.IO;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
|
||
public class TiledMapScreenshotTool : EditorWindow
|
||
{
|
||
[Header("相机设置")]
|
||
public Camera targetCamera;
|
||
public LayerMask layerMask = -1;
|
||
|
||
[Header("区域设置")]
|
||
public Vector3 startPosition = new Vector3(-100, 0, -100);
|
||
public Vector3 endPosition = new Vector3(100, 0, 100);
|
||
public float cameraHeight = 100f;
|
||
|
||
[Header("截图设置")]
|
||
public int tileWidth = 2048; // 单个瓦片宽度
|
||
public int tileHeight = 2048; // 单个瓦片高度
|
||
public int antiAliasing = 4; // 抗锯齿
|
||
public float overlapPercent = 5f; // 重叠百分比,避免接缝
|
||
|
||
[Header("网格预览")]
|
||
public bool showGridPreview = true;
|
||
public Color gridColor = Color.yellow;
|
||
|
||
[Header("输出设置")]
|
||
public string fileName = "SuperMapScreenshot";
|
||
public string outputPath = "Assets/Screenshots/";
|
||
public bool saveIndividualTiles = false; // 是否保存单个瓦片
|
||
|
||
private Vector3 originalCameraPosition;
|
||
private Quaternion originalCameraRotation;
|
||
private bool originalOrthographic;
|
||
private float originalOrthographicSize;
|
||
private int originalCullingMask;
|
||
|
||
private int gridWidth, gridHeight;
|
||
private float tileWorldWidth, tileWorldHeight;
|
||
private Vector2 totalWorldSize;
|
||
private int finalImageWidth, finalImageHeight;
|
||
|
||
[MenuItem("Tools/分块地图截图工具")]
|
||
public static void ShowWindow()
|
||
{
|
||
GetWindow<TiledMapScreenshotTool>("分块地图截图工具");
|
||
}
|
||
|
||
void OnEnable()
|
||
{
|
||
SceneView.duringSceneGui += OnSceneGUI;
|
||
CalculateGrid();
|
||
}
|
||
|
||
void OnDisable()
|
||
{
|
||
SceneView.duringSceneGui -= OnSceneGUI;
|
||
}
|
||
|
||
void OnGUI()
|
||
{
|
||
GUILayout.Label("分块地图截图工具", EditorStyles.boldLabel);
|
||
EditorGUILayout.Space();
|
||
|
||
// 相机设置
|
||
EditorGUILayout.LabelField("相机设置", EditorStyles.boldLabel);
|
||
targetCamera = (Camera)EditorGUILayout.ObjectField("目标相机", targetCamera, typeof(Camera), true);
|
||
|
||
if (targetCamera == null)
|
||
{
|
||
EditorGUILayout.HelpBox("请选择一个相机作为截图相机", MessageType.Warning);
|
||
if (GUILayout.Button("使用场景相机"))
|
||
{
|
||
targetCamera = SceneView.lastActiveSceneView?.camera;
|
||
}
|
||
}
|
||
|
||
layerMask = EditorGUILayout.MaskField("渲染层级", layerMask, UnityEditorInternal.InternalEditorUtility.layers);
|
||
|
||
EditorGUILayout.Space();
|
||
|
||
// 区域设置
|
||
EditorGUILayout.LabelField("截图区域设置", EditorStyles.boldLabel);
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
if (GUILayout.Button("设为当前相机位置", GUILayout.Width(120)))
|
||
{
|
||
if (targetCamera != null)
|
||
{
|
||
Vector3 pos = targetCamera.transform.position;
|
||
startPosition = new Vector3(pos.x - 50, startPosition.y, pos.z - 50);
|
||
}
|
||
}
|
||
startPosition = EditorGUILayout.Vector3Field("开始位置 (XZ)", startPosition);
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
if (GUILayout.Button("设为当前相机位置", GUILayout.Width(120)))
|
||
{
|
||
if (targetCamera != null)
|
||
{
|
||
Vector3 pos = targetCamera.transform.position;
|
||
endPosition = new Vector3(pos.x + 50, endPosition.y, pos.z + 50);
|
||
}
|
||
}
|
||
endPosition = EditorGUILayout.Vector3Field("结束位置 (XZ)", endPosition);
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
cameraHeight = EditorGUILayout.FloatField("相机高度", cameraHeight);
|
||
|
||
EditorGUILayout.Space();
|
||
|
||
// 截图设置
|
||
EditorGUILayout.LabelField("瓦片设置", EditorStyles.boldLabel);
|
||
|
||
EditorGUI.BeginChangeCheck();
|
||
tileWidth = EditorGUILayout.IntField("瓦片宽度", tileWidth);
|
||
tileHeight = EditorGUILayout.IntField("瓦片高度", tileHeight);
|
||
antiAliasing = EditorGUILayout.IntPopup("抗锯齿", antiAliasing, new string[] {"无", "2x", "4x", "8x"}, new int[] {1, 2, 4, 8});
|
||
overlapPercent = EditorGUILayout.Slider("重叠百分比", overlapPercent, 0f, 20f);
|
||
|
||
if (EditorGUI.EndChangeCheck())
|
||
{
|
||
CalculateGrid();
|
||
}
|
||
|
||
EditorGUILayout.Space();
|
||
|
||
// 网格预览
|
||
EditorGUILayout.LabelField("网格预览", EditorStyles.boldLabel);
|
||
showGridPreview = EditorGUILayout.Toggle("显示网格预览", showGridPreview);
|
||
if (showGridPreview)
|
||
{
|
||
gridColor = EditorGUILayout.ColorField("网格颜色", gridColor);
|
||
}
|
||
|
||
EditorGUILayout.Space();
|
||
|
||
// 计算信息显示
|
||
EditorGUILayout.LabelField("计算信息", EditorStyles.boldLabel);
|
||
EditorGUILayout.LabelField($"世界区域大小: {totalWorldSize.x:F1} x {totalWorldSize.y:F1}");
|
||
EditorGUILayout.LabelField($"网格数量: {gridWidth} x {gridHeight} = {gridWidth * gridHeight} 张");
|
||
EditorGUILayout.LabelField($"最终图片尺寸: {finalImageWidth} x {finalImageHeight}");
|
||
EditorGUILayout.LabelField($"预估文件大小: ~{(finalImageWidth * finalImageHeight * 4 / 1024f / 1024f):F1} MB");
|
||
|
||
if (gridWidth * gridHeight > 100)
|
||
{
|
||
EditorGUILayout.HelpBox($"瓦片数量较多({gridWidth * gridHeight}张),截图可能需要较长时间", MessageType.Warning);
|
||
}
|
||
|
||
EditorGUILayout.Space();
|
||
|
||
// 输出设置
|
||
EditorGUILayout.LabelField("输出设置", EditorStyles.boldLabel);
|
||
fileName = EditorGUILayout.TextField("文件名", fileName);
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
outputPath = EditorGUILayout.TextField("输出路径", outputPath);
|
||
if (GUILayout.Button("选择", GUILayout.Width(50)))
|
||
{
|
||
string selectedPath = EditorUtility.OpenFolderPanel("选择输出文件夹", "Assets", "");
|
||
if (!string.IsNullOrEmpty(selectedPath))
|
||
{
|
||
outputPath = "Assets" + selectedPath.Substring(Application.dataPath.Length) + "/";
|
||
}
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
saveIndividualTiles = EditorGUILayout.Toggle("保存单个瓦片", saveIndividualTiles);
|
||
|
||
EditorGUILayout.Space();
|
||
|
||
// 操作按钮
|
||
GUI.enabled = targetCamera != null;
|
||
|
||
if (GUILayout.Button("预览第一个瓦片位置", GUILayout.Height(25)))
|
||
{
|
||
PreviewTilePosition(0, 0);
|
||
}
|
||
|
||
if (GUILayout.Button("开始分块截图", GUILayout.Height(40)))
|
||
{
|
||
StartTiledScreenshot();
|
||
}
|
||
|
||
GUI.enabled = true;
|
||
}
|
||
|
||
void CalculateGrid()
|
||
{
|
||
// 计算世界区域大小
|
||
totalWorldSize = new Vector2(
|
||
Mathf.Abs(endPosition.x - startPosition.x),
|
||
Mathf.Abs(endPosition.z - startPosition.z)
|
||
);
|
||
|
||
// 计算每个瓦片对应的世界大小
|
||
float aspectRatio = (float)tileWidth / tileHeight;
|
||
|
||
// 根据重叠百分比调整实际覆盖区域
|
||
float overlapFactor = 1f - (overlapPercent / 100f);
|
||
|
||
// 估算合适的瓦片世界大小
|
||
tileWorldWidth = totalWorldSize.x / Mathf.Ceil(totalWorldSize.x / (totalWorldSize.x / 10f));
|
||
tileWorldHeight = tileWorldWidth / aspectRatio;
|
||
|
||
// 计算网格数量
|
||
gridWidth = Mathf.CeilToInt(totalWorldSize.x / (tileWorldWidth * overlapFactor));
|
||
gridHeight = Mathf.CeilToInt(totalWorldSize.y / (tileWorldHeight * overlapFactor));
|
||
|
||
// 重新计算精确的瓦片世界大小
|
||
tileWorldWidth = totalWorldSize.x / gridWidth;
|
||
tileWorldHeight = totalWorldSize.y / gridHeight;
|
||
|
||
// 计算最终图片尺寸
|
||
finalImageWidth = gridWidth * tileWidth;
|
||
finalImageHeight = gridHeight * tileHeight;
|
||
|
||
// 重绘场景视图以更新网格预览
|
||
SceneView.RepaintAll();
|
||
}
|
||
|
||
void OnSceneGUI(SceneView sceneView)
|
||
{
|
||
if (!showGridPreview) return;
|
||
|
||
Handles.color = gridColor;
|
||
|
||
// 绘制整体区域边界
|
||
Vector3 min = new Vector3(startPosition.x, cameraHeight, startPosition.z);
|
||
Vector3 max = new Vector3(endPosition.x, cameraHeight, endPosition.z);
|
||
|
||
// 绘制外边框
|
||
Handles.DrawLine(new Vector3(min.x, min.y, min.z), new Vector3(max.x, min.y, min.z));
|
||
Handles.DrawLine(new Vector3(max.x, min.y, min.z), new Vector3(max.x, min.y, max.z));
|
||
Handles.DrawLine(new Vector3(max.x, min.y, max.z), new Vector3(min.x, min.y, max.z));
|
||
Handles.DrawLine(new Vector3(min.x, min.y, max.z), new Vector3(min.x, min.y, min.z));
|
||
|
||
// 绘制网格线
|
||
for (int x = 1; x < gridWidth; x++)
|
||
{
|
||
float worldX = startPosition.x + (x * tileWorldWidth);
|
||
Handles.DrawLine(
|
||
new Vector3(worldX, cameraHeight, startPosition.z),
|
||
new Vector3(worldX, cameraHeight, endPosition.z)
|
||
);
|
||
}
|
||
|
||
for (int z = 1; z < gridHeight; z++)
|
||
{
|
||
float worldZ = startPosition.z + (z * tileWorldHeight);
|
||
Handles.DrawLine(
|
||
new Vector3(startPosition.x, cameraHeight, worldZ),
|
||
new Vector3(endPosition.x, cameraHeight, worldZ)
|
||
);
|
||
}
|
||
|
||
// 绘制瓦片中心点
|
||
Handles.color = Color.red;
|
||
for (int x = 0; x < gridWidth; x++)
|
||
{
|
||
for (int z = 0; z < gridHeight; z++)
|
||
{
|
||
Vector3 tileCenter = GetTileWorldPosition(x, z);
|
||
Handles.DrawWireCube(tileCenter, Vector3.one * 2f);
|
||
Handles.Label(tileCenter + Vector3.up * 5f, $"{x},{z}");
|
||
}
|
||
}
|
||
}
|
||
|
||
Vector3 GetTileWorldPosition(int gridX, int gridZ)
|
||
{
|
||
float worldX = startPosition.x + (gridX + 0.5f) * tileWorldWidth;
|
||
float worldZ = startPosition.z + (gridZ + 0.5f) * tileWorldHeight;
|
||
return new Vector3(worldX, cameraHeight, worldZ);
|
||
}
|
||
|
||
void PreviewTilePosition(int gridX, int gridZ)
|
||
{
|
||
if (targetCamera == null) return;
|
||
|
||
Vector3 tileCenter = GetTileWorldPosition(gridX, gridZ);
|
||
|
||
// 移动场景视图到瓦片位置
|
||
SceneView sceneView = SceneView.lastActiveSceneView;
|
||
if (sceneView != null)
|
||
{
|
||
sceneView.pivot = tileCenter;
|
||
sceneView.rotation = Quaternion.Euler(90, 0, 0);
|
||
sceneView.size = Mathf.Max(tileWorldWidth, tileWorldHeight) / 2f;
|
||
sceneView.orthographic = true;
|
||
sceneView.Repaint();
|
||
}
|
||
}
|
||
|
||
void StartTiledScreenshot()
|
||
{
|
||
if (targetCamera == null)
|
||
{
|
||
EditorUtility.DisplayDialog("错误", "请先选择一个相机", "确定");
|
||
return;
|
||
}
|
||
|
||
// 确保输出目录存在
|
||
if (!Directory.Exists(outputPath))
|
||
{
|
||
Directory.CreateDirectory(outputPath);
|
||
}
|
||
|
||
EditorCoroutineUtility.StartCoroutine(TakeScreenshotCoroutine(), this);
|
||
}
|
||
|
||
IEnumerator TakeScreenshotCoroutine()
|
||
{
|
||
// 使用二维数组存储瓦片,保持空间位置关系
|
||
Texture2D[,] tileTextures = new Texture2D[gridWidth, gridHeight];
|
||
|
||
// try
|
||
{
|
||
// 保存原始相机设置
|
||
SaveOriginalCameraSettings();
|
||
|
||
// 设置相机为正交模式
|
||
targetCamera.orthographic = true;
|
||
targetCamera.transform.rotation = Quaternion.Euler(90, 0, 0);
|
||
targetCamera.cullingMask = layerMask;
|
||
targetCamera.orthographicSize = Mathf.Max(tileWorldWidth, tileWorldHeight) / 2f;
|
||
targetCamera.nearClipPlane = 0.1f;
|
||
targetCamera.farClipPlane = cameraHeight + 50f;
|
||
|
||
int totalTiles = gridWidth * gridHeight;
|
||
int currentTile = 0;
|
||
|
||
// 逐个拍摄瓦片
|
||
for (int z = 0; z < gridHeight; z++)
|
||
{
|
||
for (int x = 0; x < gridWidth; x++)
|
||
{
|
||
currentTile++;
|
||
float progress = (float)currentTile / totalTiles;
|
||
|
||
if (EditorUtility.DisplayCancelableProgressBar(
|
||
"分块截图中",
|
||
$"正在截图瓦片 {currentTile}/{totalTiles} ({x},{z})",
|
||
progress))
|
||
{
|
||
break;
|
||
}
|
||
|
||
// 移动相机到瓦片位置
|
||
Vector3 tilePosition = GetTileWorldPosition(x, z);
|
||
targetCamera.transform.position = tilePosition;
|
||
|
||
// 截图
|
||
Texture2D tileTexture = CaptureScreenshot();
|
||
if (tileTexture != null)
|
||
{
|
||
// 存储到对应位置
|
||
tileTextures[x, z] = tileTexture;
|
||
|
||
// 如果需要保存单个瓦片
|
||
if (saveIndividualTiles)
|
||
{
|
||
byte[] bytes = tileTexture.EncodeToPNG();
|
||
string tilePath = Path.Combine(outputPath, $"{fileName}_tile_{x}_{z}.png");
|
||
File.WriteAllBytes(tilePath, bytes);
|
||
}
|
||
}
|
||
|
||
yield return null; // 等待一帧
|
||
}
|
||
}
|
||
|
||
EditorUtility.DisplayProgressBar("合成图片", "正在合成最终图片...", 1f);
|
||
|
||
// 合成最终图片
|
||
Texture2D finalTexture = CombineTiles(tileTextures);
|
||
if (finalTexture != null)
|
||
{
|
||
// 保存最终图片
|
||
byte[] finalBytes = finalTexture.EncodeToPNG();
|
||
string finalPath = Path.Combine(outputPath, $"{fileName}_{finalImageWidth}x{finalImageHeight}.png");
|
||
File.WriteAllBytes(finalPath, finalBytes);
|
||
|
||
DestroyImmediate(finalTexture);
|
||
|
||
EditorUtility.DisplayDialog("成功",
|
||
$"超大清晰地图已保存!\n" +
|
||
$"文件: {finalPath}\n" +
|
||
$"尺寸: {finalImageWidth}x{finalImageHeight}\n" +
|
||
$"瓦片数: {totalTiles}", "确定");
|
||
}
|
||
|
||
}
|
||
// catch (System.Exception e)
|
||
// {
|
||
// EditorUtility.DisplayDialog("错误", $"截图失败: {e.Message}", "确定");
|
||
// Debug.LogError($"分块截图失败: {e}");
|
||
// }
|
||
// finally
|
||
{
|
||
// 清理资源
|
||
for (int x = 0; x < gridWidth; x++)
|
||
{
|
||
for (int z = 0; z < gridHeight; z++)
|
||
{
|
||
if (tileTextures[x, z] != null)
|
||
DestroyImmediate(tileTextures[x, z]);
|
||
}
|
||
}
|
||
|
||
RestoreOriginalCameraSettings();
|
||
EditorUtility.ClearProgressBar();
|
||
AssetDatabase.Refresh();
|
||
}
|
||
}
|
||
|
||
Texture2D CaptureScreenshot()
|
||
{
|
||
// 创建渲染纹理
|
||
RenderTexture renderTexture = new RenderTexture(tileWidth, tileHeight, 24);
|
||
renderTexture.antiAliasing = antiAliasing;
|
||
renderTexture.Create();
|
||
|
||
// 渲染
|
||
RenderTexture originalTarget = targetCamera.targetTexture;
|
||
targetCamera.targetTexture = renderTexture;
|
||
targetCamera.Render();
|
||
|
||
// 读取像素
|
||
RenderTexture.active = renderTexture;
|
||
Texture2D screenshot = new Texture2D(tileWidth, tileHeight, TextureFormat.RGB24, false);
|
||
screenshot.ReadPixels(new Rect(0, 0, tileWidth, tileHeight), 0, 0);
|
||
screenshot.Apply();
|
||
|
||
// 清理
|
||
RenderTexture.active = null;
|
||
targetCamera.targetTexture = originalTarget;
|
||
renderTexture.Release();
|
||
DestroyImmediate(renderTexture);
|
||
|
||
return screenshot;
|
||
}
|
||
|
||
Texture2D CombineTiles(Texture2D[,] tiles)
|
||
{
|
||
Texture2D finalTexture = new Texture2D(finalImageWidth, finalImageHeight, TextureFormat.RGB24, false);
|
||
|
||
// 关键修复:不翻转Z轴,保持原始顺序
|
||
for (int z = 0; z < gridHeight; z++)
|
||
{
|
||
for (int x = 0; x < gridWidth; x++)
|
||
{
|
||
Texture2D tile = tiles[x, z];
|
||
if (tile == null) continue;
|
||
|
||
Color[] pixels = tile.GetPixels();
|
||
|
||
// 修复:直接映射,不进行Y轴翻转
|
||
// 世界坐标系的Z=0对应纹理的Y=0(底部)
|
||
// 世界坐标系的Z=max对应纹理的Y=max(顶部)
|
||
int startX = x * tileWidth;
|
||
int startY = z * tileHeight; // 移除翻转:直接使用z
|
||
|
||
finalTexture.SetPixels(startX, startY, tileWidth, tileHeight, pixels);
|
||
}
|
||
}
|
||
|
||
finalTexture.Apply();
|
||
return finalTexture;
|
||
}
|
||
|
||
void SaveOriginalCameraSettings()
|
||
{
|
||
if (targetCamera == null) return;
|
||
|
||
originalCameraPosition = targetCamera.transform.position;
|
||
originalCameraRotation = targetCamera.transform.rotation;
|
||
originalOrthographic = targetCamera.orthographic;
|
||
originalOrthographicSize = targetCamera.orthographicSize;
|
||
originalCullingMask = targetCamera.cullingMask;
|
||
}
|
||
|
||
void RestoreOriginalCameraSettings()
|
||
{
|
||
if (targetCamera == null) return;
|
||
|
||
targetCamera.transform.position = originalCameraPosition;
|
||
targetCamera.transform.rotation = originalCameraRotation;
|
||
targetCamera.orthographic = originalOrthographic;
|
||
targetCamera.orthographicSize = originalOrthographicSize;
|
||
targetCamera.cullingMask = originalCullingMask;
|
||
}
|
||
|
||
void OnDestroy()
|
||
{
|
||
RestoreOriginalCameraSettings();
|
||
}
|
||
}
|
||
|
||
// 编辑器协程工具类
|
||
public static class EditorCoroutineUtility
|
||
{
|
||
public static EditorCoroutine StartCoroutine(IEnumerator routine, object owner)
|
||
{
|
||
return new EditorCoroutine(routine, owner);
|
||
}
|
||
}
|
||
|
||
public class EditorCoroutine
|
||
{
|
||
private IEnumerator routine;
|
||
private object owner;
|
||
|
||
public EditorCoroutine(IEnumerator routine, object owner)
|
||
{
|
||
this.routine = routine;
|
||
this.owner = owner;
|
||
EditorApplication.update += Update;
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
if (routine != null)
|
||
{
|
||
if (!routine.MoveNext())
|
||
{
|
||
EditorApplication.update -= Update;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
EditorApplication.update -= Update;
|
||
}
|
||
}
|
||
} |