Unity自动化批量输出模型截图
1.需求
最近又接了一个奇怪的需求:Unity运行状态下,自动化批量输出Game视图中的模型的前、侧、左、后截图,以及特效的GIF图
2.分析
批量
读取文件夹内所有模型预置
GIF图
1.Unity是不识别Gif格式图,优选插件:NatCorder
2.特效预置上需标明自身生命周期的脚本,以便调整合适GIF时长
PNG图
NatCorder支持JPG输出,改变输出的格式即可
相机自动聚焦
1.参考Scene视图快捷F及组合键实现原理:SceneView.lastActiveSceneView.FrameSelected();
2.参考Github的开源代码:关键字 FOCUS CAMERA ON OBJECT
运行流程
循环部分:
第一步.相机自动聚焦
第二步.开始截图/录制
第三步.结束截图/录制
简易方式:
协程队列依次执行
3.实现
协程队列ActionQueue 继承 MonoBehaviour
private event Action onComplete;
private List<OneAction> actions = new List<OneAction>();
public static ActionQueue InitOneActionQueue()
{
return new GameObject().AddComponent<ActionQueue>();
}
/// <summary>
/// 添加一个任务到队列
/// </summary>
/// <param name="startAction">开始时执行的方法</param>
/// <param name="IsCompleted">判断该节点是否完成</param>
/// <returns></returns>
public ActionQueue AddAction(Action startAction, Func<bool> IsCompleted)
{
actions.Add(new OneAction(startAction, IsCompleted));
return this;
}
/// <summary>
/// 添加一个协程方法到队列
/// </summary>
/// <param name="enumerator">一个协程</param>
/// <returns></returns>
public ActionQueue AddAction(IEnumerator enumerator)
{
actions.Add(new OneAction(enumerator));
return this;
}
/// <summary>
/// 添加一个任务到队列
/// </summary>
/// <param name="action">一个方法</param>
/// <returns></returns>
public ActionQueue AddAction(Action action)
{
actions.Add(new OneAction(action));
return this;
}
/// <summary>
/// 绑定执行完毕回调
/// </summary>
/// <param name="callback"></param>
/// <returns></returns>
public ActionQueue BindCallback(Action callback)
{
onComplete += callback;
return this;
}
/// <summary>
/// 开始执行队列
/// </summary>
/// <returns></returns>
public ActionQueue StartQueue()
{
StartCoroutine(StartQueueAsync());
return this;
}
private IEnumerator StartQueueAsync()
{
if (actions.Count > 0)
{
if (actions[0].startAction != null)
{
actions[0].startAction();
}
}
while (actions.Count > 0)
{
yield return actions[0].enumerator;
actions.RemoveAt(0);
if (actions.Count > 0)
{
if (actions[0].startAction != null)
{
actions[0].startAction();
}
}
else
{
break;
}
yield return new WaitForEndOfFrame();
}
if (onComplete != null)
{
onComplete();
}
Destroy(gameObject);
}
private class OneAction
{
public Action startAction;
public IEnumerator enumerator;
public OneAction(Action startAction, Func<bool> IsCompleted)
{
this.startAction = startAction;
//如果没用协程,自己创建一个协程
enumerator = new CustomEnumerator(IsCompleted);
}
public OneAction(IEnumerator enumerator, Action action = null)
{
this.startAction = action;
this.enumerator = enumerator;
}
public OneAction(Action action)
{
this.startAction = action;
this.enumerator = null;
}
/// <summary>
/// 自定义的协程
/// </summary>
private class CustomEnumerator : IEnumerator
{
public object Current => null;
private Func<bool> IsCompleted;
public CustomEnumerator(Func<bool> IsCompleted)
{
this.IsCompleted = IsCompleted;
}
public bool MoveNext()
{
return !IsCompleted();
}
public void Reset()
{
}
}
}
读取文件夹中的预置的路径
public static string[] GetAllPrefabs(UnityEngine.Object obj)
{
var directory = AssetDatabase.GetAssetPath(obj);
if (string.IsNullOrEmpty(directory) || !directory.StartsWith("Assets"))
throw new ArgumentException("folderPath");
string[] subFolders = Directory.GetDirectories(directory);
string[] guids = null;
string[] assetPaths = null;
List<string> assetPathsList = new List<string>();//2021-4-29
int i = 0, iMax = 0;
foreach (var folder in subFolders)
{
guids = AssetDatabase.FindAssets("t:Prefab", new string[] { folder });
assetPaths = new string[guids.Length];
for (i = 0, iMax = assetPaths.Length; i < iMax; ++i)
{
assetPaths[i] = AssetDatabase.GUIDToAssetPath(guids[i]);
assetPathsList.Add(assetPaths[i]);//2021-4-29
}
}
if (subFolders.Length == 0)
{
guids = AssetDatabase.FindAssets("t:Prefab", new string[] { directory });
assetPaths = new string[guids.Length];
for (i = 0, iMax = assetPaths.Length; i < iMax; ++i)
{
assetPaths[i] = AssetDatabase.GUIDToAssetPath(guids[i]);
assetPathsList.Add(assetPaths[i]);
}
}
return assetPathsList.ToArray();
}
相机自动聚焦
private static Bounds CalculateBounds(GameObject go)
{
Bounds b = new Bounds(go.transform.position, Vector3.zero);
UnityEngine.Object[] rList = go.GetComponentsInChildren(typeof(Renderer));
foreach (Renderer r in rList)
{
b.Encapsulate(r.bounds);
}
return b;
}
public static Vector3 FocusCameraOnGameObject(Camera c, GameObject go)
{
Bounds b = CalculateBounds(go);
Vector3 max = b.size;
// Get the radius of a sphere circumscribing the bounds
float radius = max.magnitude / 2f;
// Get the horizontal FOV, since it may be the limiting of the two FOVs to properly encapsulate the objects
float horizontalFOV = 2f * Mathf.Atan(Mathf.Tan(c.fieldOfView * Mathf.Deg2Rad / 2f) * c.aspect) * Mathf.Rad2Deg;
// Use the smaller FOV as it limits what would get cut off by the frustum
float fov = Mathf.Min(c.fieldOfView, horizontalFOV);
float dist = radius / (Mathf.Sin(fov * Mathf.Deg2Rad / 2f));
Debug.Log("Radius = " + radius + " dist = " + dist);
c.transform.localPosition = new Vector3(b.center.x, b.center.y, b.center.z - dist);
if (c.orthographic)
c.orthographicSize = radius;
// Frame the object hierarchy
c.transform.LookAt(b.center);
var pos = new Vector3(c.transform.localPosition.x, c.transform.localPosition.y, dist);
return pos;
}
JPG输出继承 MonoBehaviour
public int imageWidth = 640;
public int imageHeight = 480;
private PNGRecorder m_PNGRecorder;
private CameraInput cameraInput;
private bool isPhoto = false;
public float autoFocusTime = 3f;
private float frameDuration = 0.1f;
public UnityEngine.Object m_ModelDirectory;
private int pngIndex = 0;
private Vector3 rotateAngle = Vector3.zero;
private GameObject target = null;
private Transform targetTrans = null;
private IEnumerator WaitPNGAutoFocus(GameObject obj)
{
Debug.Log(string.Format("<color=red>{0}</color>", " Start " + obj.name + " Auto Focus "));
int reIndex = pngIndex % 4;
switch (reIndex)
{
case 1:
rotateAngle.y = 45f;
targetTrans.eulerAngles = rotateAngle;
break;
case 2:
rotateAngle.y = 90f;
targetTrans.eulerAngles = rotateAngle;
break;
case 3:
rotateAngle.y = 180f;
targetTrans.eulerAngles = rotateAngle;
break;
case 0:
if (target) Destroy(target);
target = GameObject.Instantiate(obj);
targetTrans = target.transform;
rotateAngle = targetTrans.eulerAngles;
pngIndex = 0;
break;
}
if (target) FocusCameraOnGameObject(Camera.main, target);
yield return new WaitForSeconds(autoFocusTime);
Debug.Log(string.Format("<color=green>{0}</color>", " Over " + obj.name + " Auto Focus "));
pngIndex++;
}
private IEnumerator PNGRecording(string extension)
{
if (isPhoto == false)
{
isPhoto = true;
// Start recording
m_PNGRecorder = new PNGRecorder(imageWidth, imageHeight, target.name + extension);
cameraInput = new CameraInput(m_PNGRecorder, new RealtimeClock(), Camera.main);
cameraInput.frameSkip = 40;
Debug.Log(string.Format("<color=blue>{0}</color>", " Start PNG Recording "));
}
yield return new WaitForSeconds(frameDuration);
isPhoto = false;
// Stop the recording
cameraInput.Dispose();
m_PNGRecorder.FinishWriting();
Debug.Log(string.Format("<color=yellow>{0}</color>", " Over PNG Recording "));
}
void Start()
{
actionQueue = ActionQueue.InitOneActionQueue();
{
foreach (string assetPath in GetAllPrefabs(m_ModelDirectory))
{
GameObject assertObj = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
actionQueue = actionQueue.AddAction(WaitPNGAutoFocus(assertObj)).AddAction(JPGRecording("_Front"))
.AddAction(WaitPNGAutoFocus(assertObj)).AddAction(JPGRecording("_Side"))
.AddAction(WaitPNGAutoFocus(assertObj)).AddAction(JPGRecording("_Left"))
.AddAction(WaitPNGAutoFocus(assertObj)).AddAction(JPGRecording("_Back"));
}
}
actionQueue.StartQueue();
}
GIF输出继承 MonoBehaviour
private GIFRecorder m_GIFRecorder;
private CameraInput cameraInput;
public int imageWidth = 640;
public int imageHeight = 480;
public float frameDuration = 0.1f;
public float times = 3f;
public UnityEngine.Object m_EffectDirectory;
private GameObject target = null;
private Transform targetTrans = null;
private ActionQueue actionQueue = null;
private DestoryByTime destoryByTime = null;
private IEnumerator GIFRecording(GameObject obj)
{
Debug.Log(string.Format("<color=blue>{0}</color>", " Start GIF Recording "));
target = GameObject.Instantiate(obj);
targetTrans = target.transform;
destoryByTime = targetTrans.GetComponent<DestoryByTime>();
if (destoryByTime)
{
times = destoryByTime.time;
}
// Start recording
m_GIFRecorder = new GIFRecorder(imageWidth, imageHeight, frameDuration);
cameraInput = new CameraInput(m_GIFRecorder, new RealtimeClock(), Camera.main);
// Get a real GIF look by skipping frames
cameraInput.frameSkip = 4;
yield return new WaitForSeconds(times);
// Stop the recording
cameraInput.Dispose();
m_GIFRecorder.FinishWriting();
Debug.Log(string.Format("<color=yellow>{0}</color>", " Over GIF Recording "));
}
private void Start()
{
actionQueue = ActionQueue.InitOneActionQueue();
foreach (string assetPath in GetAllPrefabs(m_EffectDirectory))
{
GameObject assertObj = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
actionQueue = actionQueue.AddAction(GIFRecording(assertObj));
}
actionQueue.StartQueue();
}
脚本DestoryByTime继承 MonoBehaviour
public bool startActive = true;
public float time = 5f;
private void Start()
{
if (startActive) Destroy(gameObject, time);
}
private void Update()
{
if (startActive) Destroy(gameObject, time);
}
脚本PNGRecorder
复制插件NatCorder中JPGRecorder脚本重命名为PNGRecorder ,修改输出的后缀.jpg为.png
注:调用API命名空间