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命名空间

posted @ 2021-05-13 11:46  微臣做不到  阅读(566)  评论(0编辑  收藏  举报