419 lines
14 KiB
C#
419 lines
14 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Reflection;
|
||
using UnityEngine;
|
||
using UnityEditor.Recorder;
|
||
using UnityEditor.Recorder.Input;
|
||
|
||
#if UNITY_EDITOR
|
||
using UnityEditor;
|
||
#endif
|
||
|
||
/// <summary>
|
||
/// 自动批量录制Prefab的工具
|
||
/// </summary>
|
||
public class PrefabRecorderTool : MonoBehaviour
|
||
{
|
||
[Header("录制设置")]
|
||
[Tooltip("需要录制的Prefab所在的文件夹路径(相对于Assets目录)")]
|
||
public string prefabFolderPath = "Assets/Art/VFX";
|
||
|
||
[Tooltip("每个Prefab录制的次数")]
|
||
public int recordCountPerPrefab = 3;
|
||
|
||
[Tooltip("每次录制的时长(秒)")]
|
||
public float recordDuration = 3f;
|
||
|
||
[Tooltip("Prefab实例化的位置")]
|
||
public Vector3 spawnPosition = Vector3.zero;
|
||
|
||
[Tooltip("Prefab实例化的旋转")]
|
||
public Vector3 spawnRotation = Vector3.zero;
|
||
|
||
[Tooltip("录制输出的根目录")]
|
||
public string outputRootFolder = "Recordings";
|
||
|
||
[Tooltip("自动开始录制")]
|
||
public bool autoStartOnPlay = false;
|
||
|
||
[Header("录制器设置")]
|
||
[Tooltip("使用现有的Recorder Controller设置(可选,留空则使用下方参数创建新设置)")]
|
||
public RecorderControllerSettings existingRecorderSettings;
|
||
|
||
[Tooltip("录制分辨率宽度(仅在不使用现有设置时生效)")]
|
||
public int recordWidth = 1920;
|
||
|
||
[Tooltip("录制分辨率高度(仅在不使用现有设置时生效)")]
|
||
public int recordHeight = 1080;
|
||
|
||
[Tooltip("帧率(仅在不使用现有设置时生效)")]
|
||
public int frameRate = 30;
|
||
|
||
private RecorderController m_RecorderController;
|
||
private RecorderControllerSettings m_ControllerSettings;
|
||
private List<GameObject> m_PrefabList = new List<GameObject>();
|
||
private GameObject m_CurrentInstance;
|
||
private bool m_IsRecording = false;
|
||
private int m_CurrentPrefabIndex = 0;
|
||
private int m_CurrentRecordIndex = 0;
|
||
|
||
void Start()
|
||
{
|
||
#if UNITY_EDITOR
|
||
if (autoStartOnPlay)
|
||
{
|
||
StartBatchRecording();
|
||
}
|
||
#else
|
||
Debug.LogError("PrefabRecorderTool只能在编辑器模式下运行!");
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 开始批量录制
|
||
/// </summary>
|
||
public void StartBatchRecording()
|
||
{
|
||
#if UNITY_EDITOR
|
||
// 加载所有Prefab
|
||
LoadPrefabs();
|
||
|
||
if (m_PrefabList.Count == 0)
|
||
{
|
||
Debug.LogError($"在路径 {prefabFolderPath} 中没有找到任何Prefab!");
|
||
return;
|
||
}
|
||
|
||
Debug.Log($"找到 {m_PrefabList.Count} 个Prefab,开始批量录制...");
|
||
|
||
// 初始化录制控制器
|
||
InitializeRecorder();
|
||
|
||
// 开始录制第一个Prefab
|
||
m_CurrentPrefabIndex = 0;
|
||
m_CurrentRecordIndex = 0;
|
||
StartCoroutine(RecordNextPrefab());
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 加载指定文件夹中的所有Prefab
|
||
/// </summary>
|
||
void LoadPrefabs()
|
||
{
|
||
#if UNITY_EDITOR
|
||
m_PrefabList.Clear();
|
||
|
||
if (!Directory.Exists(prefabFolderPath))
|
||
{
|
||
Debug.LogError($"路径不存在: {prefabFolderPath}");
|
||
return;
|
||
}
|
||
|
||
// 获取文件夹中所有的prefab文件
|
||
string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { prefabFolderPath });
|
||
|
||
foreach (string guid in prefabGuids)
|
||
{
|
||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||
|
||
if (prefab != null)
|
||
{
|
||
m_PrefabList.Add(prefab);
|
||
Debug.Log($"加载Prefab: {prefab.name}");
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化录制器
|
||
/// </summary>
|
||
void InitializeRecorder()
|
||
{
|
||
#if UNITY_EDITOR
|
||
if (existingRecorderSettings != null)
|
||
{
|
||
// 使用现有的Recorder设置
|
||
Debug.Log("使用现有的Recorder设置");
|
||
m_ControllerSettings = existingRecorderSettings;
|
||
m_RecorderController = new RecorderController(m_ControllerSettings);
|
||
}
|
||
else
|
||
{
|
||
// 创建新的录制器控制器设置
|
||
Debug.Log("创建新的Recorder设置");
|
||
m_ControllerSettings = ScriptableObject.CreateInstance<RecorderControllerSettings>();
|
||
m_RecorderController = new RecorderController(m_ControllerSettings);
|
||
|
||
// 创建图像序列录制器设置
|
||
var imageRecorder = ScriptableObject.CreateInstance<ImageRecorderSettings>();
|
||
imageRecorder.name = "Image Sequence Recorder";
|
||
imageRecorder.Enabled = true;
|
||
|
||
// 设置输出格式
|
||
imageRecorder.OutputFormat = ImageRecorderSettings.ImageRecorderOutputFormat.PNG;
|
||
imageRecorder.CaptureAlpha = true; // 启用透明通道
|
||
|
||
// 设置输入源(Texture Sampling)
|
||
var renderTextureSampler = new RenderTextureSamplerSettings();
|
||
|
||
// 设置相机源为 MainCamera(使用反射访问internal字段)
|
||
var sourceField = typeof(RenderTextureSamplerSettings).GetField("source", BindingFlags.NonPublic | BindingFlags.Instance);
|
||
if (sourceField != null)
|
||
{
|
||
sourceField.SetValue(renderTextureSampler, ImageSource.MainCamera);
|
||
}
|
||
renderTextureSampler.CameraTag = "MainCamera";
|
||
|
||
// 设置渲染分辨率为 QHD (2560x2560, 1:1 宽高比)
|
||
renderTextureSampler.RenderWidth = 2560;
|
||
renderTextureSampler.RenderHeight = 2560;
|
||
|
||
// 设置输出分辨率为 QHD (2560x2560, 1:1 宽高比)
|
||
renderTextureSampler.OutputWidth = 2560;
|
||
renderTextureSampler.OutputHeight = 2560;
|
||
|
||
// 设置宽高比为 1:1(使用反射访问internal字段)
|
||
var aspectRatioField = typeof(RenderTextureSamplerSettings).GetField("outputAspectRatio", BindingFlags.NonPublic | BindingFlags.Instance);
|
||
if (aspectRatioField != null)
|
||
{
|
||
var aspectRatioType = aspectRatioField.FieldType;
|
||
var aspectRatio = System.Activator.CreateInstance(aspectRatioType);
|
||
|
||
// 设置 m_ImageAspect 字段为 1:1 (枚举值 6)
|
||
var imageAspectField = aspectRatioType.GetField("m_ImageAspect", BindingFlags.NonPublic | BindingFlags.Instance);
|
||
if (imageAspectField != null)
|
||
{
|
||
var imageAspectEnumType = imageAspectField.FieldType;
|
||
var x1_1Value = System.Enum.Parse(imageAspectEnumType, "x1_1");
|
||
imageAspectField.SetValue(aspectRatio, x1_1Value);
|
||
}
|
||
|
||
aspectRatioField.SetValue(renderTextureSampler, aspectRatio);
|
||
}
|
||
|
||
// 设置超采样为 X8
|
||
renderTextureSampler.SuperSampling = SuperSamplingCount.X8;
|
||
|
||
// 其他设置
|
||
renderTextureSampler.FlipFinalOutput = false;
|
||
|
||
imageRecorder.imageInputSettings = renderTextureSampler;
|
||
|
||
// 设置文件名生成器,使用帧序号
|
||
imageRecorder.FileNameGenerator.Root = OutputPath.Root.Absolute;
|
||
imageRecorder.FileNameGenerator.Leaf = ""; // 初始为空,后续会设置
|
||
// 使用通配符格式:<Take>是录制序号,<Frame>是帧序号
|
||
imageRecorder.FileNameGenerator.FileName = "Image Sequence_<Take>_<Frame>";
|
||
|
||
Debug.Log("FileNameGenerator配置完成,格式: Image Sequence_<Take>_<Frame>.png");
|
||
|
||
// 设置录制模式为帧率
|
||
m_ControllerSettings.SetRecordModeToManual();
|
||
m_ControllerSettings.FrameRate = frameRate;
|
||
|
||
// 添加录制器
|
||
m_ControllerSettings.AddRecorderSettings(imageRecorder);
|
||
}
|
||
|
||
// 注意:不在这里PrepareRecording,而是在每次录制前准备
|
||
Debug.Log("Recorder初始化完成");
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重置RecorderController,确保每次录制都从第1帧开始
|
||
/// </summary>
|
||
void ResetRecorderController()
|
||
{
|
||
#if UNITY_EDITOR
|
||
// 确保之前的录制已停止
|
||
if (m_IsRecording && m_RecorderController != null)
|
||
{
|
||
m_RecorderController.StopRecording();
|
||
m_IsRecording = false;
|
||
}
|
||
|
||
// 重新创建RecorderController实例以完全重置状态
|
||
// 使用相同的设置,但会重置所有内部状态(包括帧计数器)
|
||
m_RecorderController = new RecorderController(m_ControllerSettings);
|
||
|
||
Debug.Log("RecorderController已重置,帧计数器已归零");
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 录制下一个Prefab
|
||
/// </summary>
|
||
IEnumerator RecordNextPrefab()
|
||
{
|
||
#if UNITY_EDITOR
|
||
// 检查是否完成所有录制
|
||
if (m_CurrentPrefabIndex >= m_PrefabList.Count)
|
||
{
|
||
Debug.Log("所有Prefab录制完成!");
|
||
yield break;
|
||
}
|
||
|
||
GameObject currentPrefab = m_PrefabList[m_CurrentPrefabIndex];
|
||
|
||
// 检查当前Prefab的录制次数
|
||
if (m_CurrentRecordIndex >= recordCountPerPrefab)
|
||
{
|
||
m_CurrentPrefabIndex++;
|
||
m_CurrentRecordIndex = 0;
|
||
|
||
// 递归调用处理下一个Prefab
|
||
StartCoroutine(RecordNextPrefab());
|
||
yield break;
|
||
}
|
||
|
||
// 实例化Prefab
|
||
if (m_CurrentInstance != null)
|
||
{
|
||
Destroy(m_CurrentInstance);
|
||
}
|
||
|
||
m_CurrentInstance = Instantiate(currentPrefab, spawnPosition, Quaternion.Euler(spawnRotation));
|
||
Debug.Log($"实例化 {currentPrefab.name},第 {m_CurrentRecordIndex + 1}/{recordCountPerPrefab} 次录制");
|
||
|
||
// 等待两帧让场景稳定
|
||
yield return null;
|
||
yield return null;
|
||
|
||
// 设置输出路径(在PrepareRecording之前)
|
||
// 格式: {文件夹}/{Prefab名称}_{序号}/
|
||
string outputFolder = Path.Combine(outputRootFolder, $"{currentPrefab.name}_{m_CurrentRecordIndex + 1:D2}");
|
||
|
||
// 确保输出文件夹存在
|
||
string absoluteOutputFolder = Path.IsPathRooted(outputFolder) ? outputFolder : Path.Combine(Application.dataPath, "..", outputFolder);
|
||
absoluteOutputFolder = Path.GetFullPath(absoluteOutputFolder);
|
||
if (!Directory.Exists(absoluteOutputFolder))
|
||
{
|
||
Directory.CreateDirectory(absoluteOutputFolder);
|
||
Debug.Log($"创建输出文件夹: {absoluteOutputFolder}");
|
||
}
|
||
|
||
// 设置输出目录(不包含文件名,FileNameGenerator会处理文件名)
|
||
UpdateRecorderOutputPath(absoluteOutputFolder);
|
||
|
||
// 🔑 关键修复:重置RecorderController以清零帧计数器
|
||
// 这样每次录制都从第1帧开始,避免帧序号累积
|
||
ResetRecorderController();
|
||
Debug.Log($"RecorderController重置完成");
|
||
|
||
// 准备录制(每次录制前都需要重新准备)
|
||
m_RecorderController.PrepareRecording();
|
||
Debug.Log($"准备录制: {absoluteOutputFolder}");
|
||
|
||
// 等待一帧确保准备完成
|
||
yield return null;
|
||
|
||
// 开始录制
|
||
m_RecorderController.StartRecording();
|
||
m_IsRecording = true;
|
||
Debug.Log($"开始录制: {absoluteOutputFolder}");
|
||
|
||
// 等待录制时长
|
||
float recordedTime = 0f;
|
||
while (recordedTime < recordDuration)
|
||
{
|
||
recordedTime += Time.deltaTime;
|
||
yield return null;
|
||
}
|
||
|
||
// 停止录制
|
||
m_RecorderController.StopRecording();
|
||
m_IsRecording = false;
|
||
Debug.Log($"录制完成: {absoluteOutputFolder}");
|
||
|
||
// 等待一小段时间让文件写入完成和RecorderController完全停止
|
||
yield return new WaitForSeconds(0.5f);
|
||
|
||
// 增加录制索引
|
||
m_CurrentRecordIndex++;
|
||
|
||
// 继续录制
|
||
StartCoroutine(RecordNextPrefab());
|
||
#endif
|
||
yield return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新录制器输出路径
|
||
/// </summary>
|
||
void UpdateRecorderOutputPath(string directoryPath)
|
||
{
|
||
#if UNITY_EDITOR
|
||
var recorderSettingsList = m_RecorderController.Settings.RecorderSettings.ToList();
|
||
if (recorderSettingsList.Count > 0)
|
||
{
|
||
var settings = recorderSettingsList[0] as ImageRecorderSettings;
|
||
if (settings != null)
|
||
{
|
||
// 设置输出目录(使用FileNameGenerator的路径格式)
|
||
// Recorder会在这个目录下使用FileNameGenerator生成文件名
|
||
settings.FileNameGenerator.Root = OutputPath.Root.Absolute;
|
||
settings.FileNameGenerator.Leaf = directoryPath;
|
||
|
||
Debug.Log($"设置输出目录: {directoryPath}");
|
||
Debug.Log($"文件命名格式: {settings.FileNameGenerator.BuildAbsolutePath(null)}");
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("找不到ImageRecorderSettings,无法设置输出路径");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("RecorderSettings列表为空");
|
||
}
|
||
#endif
|
||
}
|
||
|
||
void OnDestroy()
|
||
{
|
||
// 清理
|
||
if (m_CurrentInstance != null)
|
||
{
|
||
Destroy(m_CurrentInstance);
|
||
}
|
||
|
||
if (m_IsRecording && m_RecorderController != null)
|
||
{
|
||
m_RecorderController.StopRecording();
|
||
}
|
||
}
|
||
|
||
void OnApplicationQuit()
|
||
{
|
||
OnDestroy();
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
// 在Inspector中添加按钮
|
||
[ContextMenu("开始批量录制")]
|
||
void StartRecordingFromMenu()
|
||
{
|
||
StartBatchRecording();
|
||
}
|
||
|
||
[ContextMenu("停止录制")]
|
||
void StopRecordingFromMenu()
|
||
{
|
||
if (m_IsRecording && m_RecorderController != null)
|
||
{
|
||
m_RecorderController.StopRecording();
|
||
m_IsRecording = false;
|
||
StopAllCoroutines();
|
||
Debug.Log("录制已停止");
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
|