vanCopper
4/18/2020 - 2:27 PM

Runtime ThumbnailGenerator Tool for Unity

Runtime ThumbnailGenerator Tool for Unity

// Author: vanCopper
// Date: 2020.04.16
#define DEBUG_BOUNDS
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Object = UnityEngine.Object;

public static class ThumbnailGenerator
{

    private const int THUMBNAIL_LAYER = 21;
    private static Vector3 THUMBNAIL_POSITION = new Vector3(-1000.0f, 1000.0f, -1000.0f);

    private static Camera m_RenderCamera;
    private static CameraSetup m_CameraSetup = new CameraSetup();

    private static List<Renderer> m_RenderersList = new List<Renderer>(64);
    private static List<int> m_LayersList = new List<int>(64);

    private static float m_Aspect;
    private static float m_MinX, m_MaxX, m_MinY, m_MaxY;
    private static float m_MaxDistance;

    private static Vector3 m_BoundsCenter;
    private static ProjectionPlane m_ProjectionPlaneHorizontal, m_ProjectionPlaneVertical;

    private static Camera m_internalCamera = null;
    private static Camera InternalCamera
    {
        get
        {
            if (m_internalCamera == null)
            {
                m_internalCamera = new GameObject("ThumbnailGeneratorCamera").AddComponent<Camera>();
                m_internalCamera.enabled = false;
                m_internalCamera.nearClipPlane = 0.01f;
                m_internalCamera.cullingMask = 1 << THUMBNAIL_LAYER;
                m_internalCamera.gameObject.hideFlags = HideFlags.HideAndDontSave;
            }
            return m_internalCamera;
        }
    }

    private static Camera m_ThumbnailRenderCamera;
    public static Camera ThumbnailRenderCamera
    {
        get { return m_ThumbnailRenderCamera; }
        set { m_ThumbnailRenderCamera = value; }
    }

    private static Vector3 m_ThumbnailDirection;
    public static Vector3 ThumbnailDirection
    {
        get { return m_ThumbnailDirection; }
        set { m_ThumbnailDirection = value.normalized; }
    }

    private static float m_padding;
    public static float Padding
    {
        get { return m_padding; }
        set { m_padding = Mathf.Clamp(value, -0.25f, 0.25f); }
    }

    private static Color m_backgroundColor;
    public static Color BackgroundColor
    {
        get { return m_backgroundColor; }
        set { m_backgroundColor = value; }
    }

    private static bool m_orthographicMode;
    public static bool OrthographicMode
    {
        get { return m_orthographicMode; }
        set { m_orthographicMode = value; }
    }

    private static bool m_markTextureNonReadable;
    public static bool MarkTextureNonReadable
    {
        get { return m_markTextureNonReadable; }
        set { m_markTextureNonReadable = value; }
    }

    static ThumbnailGenerator()
    {
        ThumbnailRenderCamera = null;
        ThumbnailDirection = new Vector3(-1f, -1f, -1f);
        Padding = 0.0f;
        BackgroundColor = new Color(0.3f, 0.3f, 0.3f, 0.0f);
        OrthographicMode = false;
        //MarkTextureNonReadable = true;
    }

    public static Texture2D GenerateMaterialThumbnail(Material material, PrimitiveType previewPrimitive, Shader shader, string replacementTag, int width = 64, int height = 64)
    {
        GameObject previeModel = GameObject.CreatePrimitive(previewPrimitive);
        previeModel.gameObject.hideFlags = HideFlags.HideAndDontSave;
        previeModel.GetComponent<Renderer>().sharedMaterial = material;
        try
        {
            return null;//TODO:
        }catch(Exception e)
        {
            Debug.LogException(e);
        }finally
        {
            Object.DestroyImmediate(previeModel);
        }
        return null;
    }

    public static Texture2D GenerateModelThumbnail(Transform model, int width = 64, int height = 64, bool shouldCloneModel = false)
    {
        return GenerateModelThumbnailWithShader(model, null, null, width, height, shouldCloneModel);
    }

    public static Texture2D GenerateModelThumbnailWithShader(Transform model, Shader shader, string replacementTag, int width = 64, int height = 64, bool shouldCloneModel = false)
    {
        if (model == null || model.Equals(null))
            return null;

        Texture2D result = null;

        if (!model.gameObject.scene.IsValid() || !model.gameObject.scene.isLoaded)
            shouldCloneModel = true;

        Transform previewObject;
        if (shouldCloneModel)
        {
            previewObject = (Transform)Object.Instantiate(model, null, false);
            previewObject.gameObject.hideFlags = HideFlags.HideAndDontSave;
        }
        else
        {
            previewObject = model;

            m_LayersList.Clear();
            GetLayerRecursively(previewObject);
        }

        bool isStatic = IsStatic(model);
        bool wasActive = previewObject.gameObject.activeSelf;
        Vector3 prevPos = previewObject.position;
        Quaternion prevRot = previewObject.rotation;

        try
        {
            SetupCamera();
            SetLayerRecursively(previewObject);

            if (!isStatic)
            {
                previewObject.position = THUMBNAIL_POSITION;
                previewObject.rotation = Quaternion.identity;
            }

            if (!wasActive)
                previewObject.gameObject.SetActive(true);

            Vector3 previewDir = previewObject.rotation * m_ThumbnailDirection;

            m_RenderersList.Clear();
            //previewObject.get
            previewObject.GetComponentsInChildren(m_RenderersList);

            Bounds previewBounds = new Bounds();
            bool init = false;
            for (int i = 0; i < m_RenderersList.Count; i++)
            {
                if (!m_RenderersList[i].enabled)
                    continue;

                //Debug.LogFormat("{1}:{0}",m_RenderersList[i].bounds.size, m_RenderersList[i].gameObject.name);
                if (!init)
                {
                    previewBounds = m_RenderersList[i].bounds;
                    init = true;
                }
                else
                    previewBounds.Encapsulate(m_RenderersList[i].bounds);
            }

            if (!init)
                return null;
#if DEBUG_BOUNDS
            DrawBounds(previewBounds);
#endif

            m_BoundsCenter = previewBounds.center;
            Vector3 boundsExtents = previewBounds.extents;
            Vector3 boundsSize = 2f * boundsExtents;

            m_Aspect = (float)width / height;
            m_RenderCamera.aspect = m_Aspect;
            m_RenderCamera.transform.rotation = Quaternion.LookRotation(previewDir, previewObject.up);

            float distance;
            if (m_orthographicMode)
            {
                m_RenderCamera.transform.position = m_BoundsCenter;

                m_MinX = m_MinY = Mathf.Infinity;
                m_MaxX = m_MaxY = Mathf.NegativeInfinity;

                Vector3 point = m_BoundsCenter + boundsExtents;
                ProjectBoundingBoxMinMax(point);
                point.x -= boundsSize.x;
                ProjectBoundingBoxMinMax(point);
                point.y -= boundsSize.y;
                ProjectBoundingBoxMinMax(point);
                point.x += boundsSize.x;
                ProjectBoundingBoxMinMax(point);
                point.z -= boundsSize.z;
                ProjectBoundingBoxMinMax(point);
                point.x -= boundsSize.x;
                ProjectBoundingBoxMinMax(point);
                point.y += boundsSize.y;
                ProjectBoundingBoxMinMax(point);
                point.x += boundsSize.x;
                ProjectBoundingBoxMinMax(point);

                distance = boundsExtents.magnitude + 1f;
                m_RenderCamera.orthographicSize = (1f + m_padding * 2f) * Mathf.Max(m_MaxY - m_MinY, (m_MaxX - m_MinX) / m_Aspect) * 0.5f;
            }
            else
            {
                m_ProjectionPlaneHorizontal = new ProjectionPlane(m_RenderCamera.transform.up, m_BoundsCenter);
                m_ProjectionPlaneVertical = new ProjectionPlane(m_RenderCamera.transform.right, m_BoundsCenter);

                m_MaxDistance = Mathf.NegativeInfinity;

                Vector3 point = m_BoundsCenter + boundsExtents;
                CalculateMaxDistance(point);
                point.x -= boundsSize.x;
                CalculateMaxDistance(point);
                point.y -= boundsSize.y;
                CalculateMaxDistance(point);
                point.x += boundsSize.x;
                CalculateMaxDistance(point);
                point.z -= boundsSize.z;
                CalculateMaxDistance(point);
                point.x -= boundsSize.x;
                CalculateMaxDistance(point);
                point.y += boundsSize.y;
                CalculateMaxDistance(point);
                point.x += boundsSize.x;
                CalculateMaxDistance(point);

                distance = (1.0f + m_padding * 2f) * Mathf.Sqrt(m_MaxDistance);
            }

            m_RenderCamera.transform.position = m_BoundsCenter - previewDir * distance;
            m_RenderCamera.farClipPlane = distance * 4f;

            RenderTexture temp = RenderTexture.active;
            RenderTexture renderTex = RenderTexture.GetTemporary(width, height, 16);
            RenderTexture.active = renderTex;
            if (m_backgroundColor.a < 1f)
                GL.Clear(false, true, m_backgroundColor);

            m_RenderCamera.targetTexture = renderTex;

            if (shader == null)
                m_RenderCamera.Render();
            else
                m_RenderCamera.RenderWithShader(shader, replacementTag == null ? string.Empty : replacementTag);

            m_RenderCamera.targetTexture = null;

            result = new Texture2D(width, height, m_backgroundColor.a < 1f ? TextureFormat.RGBA32 : TextureFormat.RGB24, false);
            result.ReadPixels(new Rect(0, 0, width, height), 0, 0, false);
            result.Apply(false, m_markTextureNonReadable);


            RenderTexture.active = temp;
            RenderTexture.ReleaseTemporary(renderTex);
        }
        catch (Exception e)
        {
            Debug.LogException(e);
        }
        finally
        {
            if (shouldCloneModel)
            {
                Object.DestroyImmediate(previewObject.gameObject);
            }
            else
            {
                if (!wasActive)
                    previewObject.gameObject.SetActive(false);

                if (!isStatic)
                {
                    previewObject.position = prevPos;
                    previewObject.rotation = prevRot;
                }

                int index = 0;
                SetLayerRecursively(previewObject, ref index);
            }

            if (m_RenderCamera == m_ThumbnailRenderCamera)
                m_CameraSetup.ApplySetup(m_RenderCamera);
        }

        return result;
    }

    private static void SetupCamera()
    {
        if(m_ThumbnailRenderCamera != null)
        {
            m_CameraSetup.GetSetup(m_ThumbnailRenderCamera);

            m_RenderCamera = m_ThumbnailRenderCamera;
            m_RenderCamera.nearClipPlane = 0.01f;
        }
        else { m_RenderCamera = InternalCamera; }

        m_RenderCamera.backgroundColor = m_backgroundColor;
        m_RenderCamera.orthographic = m_orthographicMode;
        m_RenderCamera.clearFlags = m_backgroundColor.a < 1.0f ? CameraClearFlags.Depth : CameraClearFlags.Color;
    }

    private static void ProjectBoundingBoxMinMax(Vector3 point)
    {
        Vector3 localPoint = m_RenderCamera.transform.InverseTransformPoint(point);
        if (localPoint.x < m_MinX)
            m_MinX = localPoint.x;
        if (localPoint.x > m_MaxX)
            m_MaxX = localPoint.x;
        if (localPoint.y < m_MinY)
            m_MinY = localPoint.y;
        if (localPoint.y > m_MaxY)
            m_MaxY = localPoint.y;
    }

    private static void CalculateMaxDistance(Vector3 point)
    {

        Vector3 intersectionPoint = m_ProjectionPlaneHorizontal.ClosestPointOnPlane(point);

        float horizontalDistance = m_ProjectionPlaneHorizontal.GetDistanceToPoint(point);
        float verticalDistance = m_ProjectionPlaneVertical.GetDistanceToPoint(point);

        float halfFrustumHeight = Mathf.Max(verticalDistance, horizontalDistance / m_Aspect);
        float distance = halfFrustumHeight / Mathf.Tan(m_RenderCamera.fieldOfView * 0.5f * Mathf.Deg2Rad);

        float distanceToCenter = (intersectionPoint - m_ThumbnailDirection * distance - m_BoundsCenter).sqrMagnitude;
        if (distanceToCenter > m_MaxDistance)
            m_MaxDistance = distanceToCenter;
    }

    private static bool IsStatic(Transform obj)
    {
        if (obj.gameObject.isStatic)
            return true;

        for (int i = 0; i < obj.childCount; i++)
        {
            if (IsStatic(obj.GetChild(i)))
                return true;
        }

        return false;
    }

    private static void SetLayerRecursively(Transform obj)
    {
        obj.gameObject.layer = THUMBNAIL_LAYER;
        for (int i = 0; i < obj.childCount; i++)
            SetLayerRecursively(obj.GetChild(i));
    }

    private static void GetLayerRecursively(Transform obj)
    {
        m_LayersList.Add(obj.gameObject.layer);
        for (int i = 0; i < obj.childCount; i++)
            GetLayerRecursively(obj.GetChild(i));
    }

    private static void SetLayerRecursively(Transform obj, ref int index)
    {
        obj.gameObject.layer = m_LayersList[index++];
        for (int i = 0; i < obj.childCount; i++)
            SetLayerRecursively(obj.GetChild(i), ref index);
    }


    private class CameraSetup
    {
        private Vector3 m_Position;
        private Quaternion m_Rotation;
        private RenderTexture m_targetTexture;
        private Color m_BackgroundColor;
        private bool m_Orthographic;
        private float m_OrthographicSize;
        private float m_farClipPlane;
        private float m_aspect;
        private CameraClearFlags m_ClearFlags;

        public void GetSetup(Camera camera)
        {
            m_Position = camera.transform.position;
            m_Rotation = camera.transform.rotation;

            m_targetTexture = camera.targetTexture;

            m_BackgroundColor = camera.backgroundColor;
            m_Orthographic = camera.orthographic;
            m_OrthographicSize = camera.orthographicSize;
            m_farClipPlane = camera.farClipPlane;
            m_aspect = camera.aspect;
            m_ClearFlags = camera.clearFlags;
        }

        public void ApplySetup(Camera camera)
        {
            camera.transform.position = m_Position;
            camera.transform.rotation = m_Rotation;

            camera.targetTexture = m_targetTexture;

            camera.backgroundColor = m_BackgroundColor;
            camera.orthographic = m_Orthographic;
            camera.orthographicSize = m_OrthographicSize;
            camera.farClipPlane = m_farClipPlane;
            camera.aspect = m_aspect;
            camera.clearFlags = m_ClearFlags;

            m_targetTexture = null;
        }


    }
    /// <summary>
    /// Source: https://github.com/MattRix/UnityDecompiled/blob/master/UnityEngine/UnityEngine/Plane.cs
    /// </summary>
    private struct ProjectionPlane
    {
        private readonly Vector3 m_Normal;
        private readonly float m_Distance;

        public ProjectionPlane(Vector3 inNormal, Vector3 inPoint)
        {
            m_Normal = Vector3.Normalize(inNormal);
            m_Distance = -Vector3.Dot(inNormal, inPoint);
        }

        public Vector3 ClosestPointOnPlane(Vector3 point)
        {
            float d = Vector3.Dot(m_Normal, point) + m_Distance;
            return point - m_Normal * d;
        }

        public float GetDistanceToPoint(Vector3 point)
        {
            float signedDistance = Vector3.Dot(m_Normal, point) + m_Distance;
            if(signedDistance < 0.0f)
            {
                signedDistance = -signedDistance;
            }

            return signedDistance;
        }
    }

#if DEBUG_BOUNDS
    private static void DrawBounds(Bounds b, float delay = 100)
    {
       
        // bottom
        var p1 = new Vector3(b.min.x, b.min.y, b.min.z);
        var p2 = new Vector3(b.max.x, b.min.y, b.min.z);
        var p3 = new Vector3(b.max.x, b.min.y, b.max.z);
        var p4 = new Vector3(b.min.x, b.min.y, b.max.z);

        Debug.DrawLine(p1, p2, Color.blue, delay);
        Debug.DrawLine(p2, p3, Color.red, delay);
        Debug.DrawLine(p3, p4, Color.yellow, delay);
        Debug.DrawLine(p4, p1, Color.magenta, delay);

        // top
        var p5 = new Vector3(b.min.x, b.max.y, b.min.z);
        var p6 = new Vector3(b.max.x, b.max.y, b.min.z);
        var p7 = new Vector3(b.max.x, b.max.y, b.max.z);
        var p8 = new Vector3(b.min.x, b.max.y, b.max.z);

        Debug.DrawLine(p5, p6, Color.blue, delay);
        Debug.DrawLine(p6, p7, Color.red, delay);
        Debug.DrawLine(p7, p8, Color.yellow, delay);
        Debug.DrawLine(p8, p5, Color.magenta, delay);

        // sides
        Debug.DrawLine(p1, p5, Color.white, delay);
        Debug.DrawLine(p2, p6, Color.gray, delay);
        Debug.DrawLine(p3, p7, Color.green, delay);
        Debug.DrawLine(p4, p8, Color.cyan, delay);
    }
#endif
}