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
}