<feat> 썸네일 추가

This commit is contained in:
2025-09-29 14:40:24 +09:00
parent 32ae146d3b
commit 146817b874
13 changed files with 83 additions and 908 deletions

View File

@@ -1,16 +0,0 @@
using PElib.Util;
using UnityEngine;
public static class FBXthumbnailGenerator
{
public static Texture2D Generate(GameObject modelPrefab)
{
if (modelPrefab == null) return null;
RuntimePreviewGenerator.OrthographicMode = true;
Texture2D thumbnail = RuntimePreviewGenerator.GenerateModelPreview(modelPrefab.transform, 320, 320);
thumbnail = TextureUtil.MakeReadableTexture(thumbnail);
return thumbnail;
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 02376366c9fb5524a900721a57c66165

View File

@@ -23,7 +23,7 @@ public class AssetDataManager : MonoBehaviour
{
if (asset.thumbnail == null && asset.prefabToPlace != null)
{
asset.thumbnail = FBXthumbnailGenerator.Generate(asset.prefabToPlace);
asset.thumbnail = ThumbnailGenerator.Generate(asset.prefabToPlace, 256, 256);
}
}
}

View File

@@ -13,6 +13,8 @@ public class LibView : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragH
_itemData = itemData;
imgThumbnail = transform.GetComponent<Image>();
if (_itemData.thumbnail != null)
{
Rect rect = new Rect(0, 0, _itemData.thumbnail.width, _itemData.thumbnail.height);

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 05df5e3e42542d74fb8b4b9ab743ceb5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,35 +0,0 @@
= Runtime Preview Generator (v1.1.3) =
Online documentation available at: https://github.com/yasirkula/UnityRuntimePreviewGenerator
E-mail: yasirkula@gmail.com
1. ABOUT
This plugin helps you generate preview textures (thumbnails) for your GameObject's or materials on the fly.
2. HOW TO
Simply call the following functions to generate thumbnails for your GameObject's or materials:
public static Texture2D RuntimePreviewGenerator.GenerateMaterialPreview( Material material, PrimitiveType previewPrimitive, int width = 64, int height = 64 );
public static Texture2D RuntimePreviewGenerator.GenerateModelPreview( Transform model, int width = 64, int height = 64, bool shouldCloneModel = false, bool shouldIgnoreParticleSystems = true );
Async versions use AsyncGPUReadback.Request so they won't work on all platforms or Graphics APIs:
public static void RuntimePreviewGenerator.GenerateMaterialPreviewAsync( Action<Texture2D> callback, Material material, PrimitiveType previewPrimitive, int width = 64, int height = 64 );
public static void RuntimePreviewGenerator.GenerateModelPreviewAsync( Action<Texture2D> callback, Transform model, int width = 64, int height = 64, bool shouldCloneModel = false, bool shouldIgnoreParticleSystems = true );
The width and height parameters define the size of the thumbnail texture. To guarantee a clear shot from the model in GenerateModelPreview function, the model is moved far away from the origin of the world (if it is not static) and then returned to its original position after the thumbnail is generated (this effect won't be visible to any scene cameras).
If shouldCloneModel parameter in GenerateModelPreview function is set to true, the model is instantiated (cloned) and the clone is used to create the thumbnail texture. This parameter is automatically set to true for prefabs. Unless you absolutely do not want your scene object to be moved, keep it as false for increased performance.
When the model has a child Particle System, the returned thumbnail may look empty. That's because Particle Systems usually have ridiculously large bounds and the camera needs to zoom out a lot to compensate that. Therefore, by default, Particle Systems are ignored while capturing thumbnails. You can set shouldIgnoreParticleSystems to false to override this behaviour.
To customize the preview texture, use the following properties (before calling the generate functions):
- RuntimePreviewGenerator.PreviewRenderCamera : Camera
- RuntimePreviewGenerator.PreviewDirection : Vector3
- RuntimePreviewGenerator.Padding : float
- RuntimePreviewGenerator.BackgroundColor : Color
- RuntimePreviewGenerator.OrthographicMode : bool
- RuntimePreviewGenerator.UseLocalBounds : bool
- RuntimePreviewGenerator.RenderSupersampling : float
- RuntimePreviewGenerator.MarkTextureNonReadable : bool

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: dad2c073f7ca2a14dbb025e9cb3bf335
timeCreated: 1563638903
licenseType: Store
TextScriptImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,3 +0,0 @@
{
"name": "RuntimePreviewGenerator.Runtime"
}

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 36ae44cc1707a0f42a2fcd5898aa569b
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,816 +0,0 @@
//#define DEBUG_BOUNDS
using System;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_2018_2_OR_NEWER
using UnityEngine.Rendering;
#endif
using Object = UnityEngine.Object;
public static class RuntimePreviewGenerator
{
private class CameraSetup
{
private Vector3 position;
private Quaternion rotation;
private Color backgroundColor;
private bool orthographic;
private float orthographicSize;
private float nearClipPlane;
private float farClipPlane;
private float aspect;
private int cullingMask;
private CameraClearFlags clearFlags;
private RenderTexture targetTexture;
//RuntimePreviewGenerator 의 설정값을 가져오는 메서드
public void GetSetup( Camera camera )
{
position = camera.transform.position;
rotation = camera.transform.rotation;
backgroundColor = camera.backgroundColor;
orthographic = camera.orthographic;
orthographicSize = camera.orthographicSize;
nearClipPlane = camera.nearClipPlane;
farClipPlane = camera.farClipPlane;
aspect = camera.aspect;
cullingMask = camera.cullingMask;
clearFlags = camera.clearFlags;
targetTexture = camera.targetTexture;
}
//카메라의 설정값을 RuntimePreviewGenerator 의 설정값으로 적용하는 메서드
public void ApplySetup( Camera camera )
{
camera.transform.position = position;
camera.transform.rotation = rotation;
camera.backgroundColor = backgroundColor;
camera.orthographic = orthographic;
camera.orthographicSize = orthographicSize;
camera.aspect = aspect;
camera.cullingMask = cullingMask;
camera.clearFlags = clearFlags;
if( nearClipPlane < camera.farClipPlane )
{
camera.nearClipPlane = nearClipPlane;
camera.farClipPlane = farClipPlane;
}
else
{
camera.farClipPlane = farClipPlane;
camera.nearClipPlane = nearClipPlane;
}
camera.targetTexture = targetTexture;
targetTexture = null;
}
}
private static int PREVIEW_LAYER => LayerMask.NameToLayer("ThumbnailObject");
private static Vector3 PREVIEW_POSITION = new Vector3( -250f, -250f, -250f );
private static Camera renderCamera;//모델을 렌더링 하는 카메라
private static readonly CameraSetup cameraSetup = new CameraSetup();//카메라의 설정 값
private static readonly Vector3[] boundingBoxPoints = new Vector3[8];
private static readonly Vector3[] localBoundsMinMax = new Vector3[2];
private static readonly List<Renderer> renderersList = new List<Renderer>( 64 );
private static readonly List<int> layersList = new List<int>( 64 );
private static Camera m_internalCamera = null;
private static Camera InternalCamera
{
get
{
if( m_internalCamera == null )
{
m_internalCamera = new GameObject( "ModelPreviewGeneratorCamera" ).AddComponent<Camera>();
m_internalCamera.enabled = false;
m_internalCamera.nearClipPlane = 0.01f;
m_internalCamera.cullingMask = 1 << PREVIEW_LAYER;
m_internalCamera.gameObject.hideFlags = HideFlags.HideAndDontSave;
}
return m_internalCamera;
}
}
private static Camera m_previewRenderCamera;
public static Camera PreviewRenderCamera
{
get { return m_previewRenderCamera; }
set { m_previewRenderCamera = value; }
}
private static Vector3 m_previewDirection = new Vector3( -0.57735f, -0.57735f, -0.57735f ); // Normalized (-1,-1,-1)
public static Vector3 PreviewDirection
{
get { return m_previewDirection; }
set { m_previewDirection = 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 = Color.white;
public static Color BackgroundColor
{
get { return m_backgroundColor; }
set { m_backgroundColor = value; }
}
private static bool m_orthographicMode = false;
public static bool OrthographicMode
{
get { return m_orthographicMode; }
set { m_orthographicMode = value; }
}
private static bool m_useLocalBounds = false;
public static bool UseLocalBounds
{
get { return m_useLocalBounds; }
set { m_useLocalBounds = value; }
}
private static float m_renderSupersampling = 1f;
public static float RenderSupersampling
{
get { return m_renderSupersampling; }
set { m_renderSupersampling = Mathf.Max( value, 0.1f ); }
}
private static bool m_markTextureNonReadable = true;
public static bool MarkTextureNonReadable
{
get { return m_markTextureNonReadable; }
set { m_markTextureNonReadable = value; }
}
public static Texture2D GenerateModelPreview( Transform model, int width = 64, int height = 64, bool shouldCloneModel = false, bool shouldIgnoreParticleSystems = true )
{
return GenerateModelPreviewInternal( model, null, null, width, height, shouldCloneModel, shouldIgnoreParticleSystems );
}
#if UNITY_2018_2_OR_NEWER
#endif
private static Texture2D GenerateModelPreviewInternal( Transform model, Shader shader, string replacementTag, int width, int height, bool shouldCloneModel, bool shouldIgnoreParticleSystems, Action<Texture2D> asyncCallback = null )
{
//만약 model 이 존재하지 않을 경우
if( !model )
{
#if UNITY_2018_2_OR_NEWER
//만약 asyncCallback(비동기 호출)이 없을 경우
if( asyncCallback != null )
//asynCallback 을 null 로 호출
asyncCallback( null );
#endif
//null 을 반환
return null;
}
Texture2D result = null;
//만약 model이 속한 장면이 유효하지 않거나, model 이 장면에 로드되지 않았을 경우
if( !model.gameObject.scene.IsValid() || !model.gameObject.scene.isLoaded )
shouldCloneModel = true;//모델의 복제 여부를 true로 변경
Transform previewObject;
//만약 shouldCloneModel(모델의 복제 여부)가 true 인 경우
if( shouldCloneModel )
{
//모델을 복제합니다.
//복제된 모델은 계층 구조에 표시되지 않고, 장면에 저장되지 않습니다.
previewObject = Object.Instantiate( model, null, false );
previewObject.gameObject.hideFlags = HideFlags.HideAndDontSave;
}
//그 이외의 경우
else
{
previewObject = model;
//layerList 를 초기화
//GetLayerRecursively 호출 - model과 model 의 모든 자식의 layer를 layerList 에 추가
layersList.Clear();
GetLayerRecursively( previewObject );
}
bool isStatic = IsStatic( model );//model 과 model 의 모든 자식중 정적객체가 있는지 여부
bool wasActive = previewObject.gameObject.activeSelf;
Vector3 prevPos = previewObject.position;
Quaternion prevRot = previewObject.rotation;
#if UNITY_2018_2_OR_NEWER
bool asyncOperationStarted = false; //비동기 작업이 시작되었는지 여부
#endif
try
{
//카메라를 설정하는 메서드 호출
//previewObject의 layer 를 PREVIEW_LAYER로 설정
SetupCamera();
SetLayerRecursively( previewObject );
//만약 정적객체가 없을 경우
if( !isStatic )
{
//previewObject 의 position, rotation 을 설정
previewObject.position = PREVIEW_POSITION;
previewObject.rotation = Quaternion.identity;
}
//만약 previewObject 가 활성화 되어있지 않은 경우
if( !wasActive )
//previewObject 를 활성화
previewObject.gameObject.SetActive( true );
Quaternion cameraRotation = Quaternion.LookRotation( previewObject.rotation * m_previewDirection, previewObject.up );
Bounds previewBounds = new Bounds();
//만약 경계 계산 메서드를 통해서, 경계를 가져온 경우
if (!CalculateBounds(previewObject, shouldIgnoreParticleSystems, cameraRotation, out previewBounds))
{
#if UNITY_2018_2_OR_NEWER
if (asyncCallback != null)
asyncCallback(null);
#endif
//null 을 반환
return null;
}
//renderCamera 의 가로,세로 비율을 조정
//renderCamera 의 회전값을 cameraRoatation 으로 지정
renderCamera.aspect = (float) width / height;
renderCamera.transform.rotation = cameraRotation;
//카메라 위치 계산 메서드 호출
CalculateCameraPosition( renderCamera, previewBounds, m_padding );
//카메라에서 멀리 떨어진 클리핑 평면의 거리 지정
renderCamera.farClipPlane = ( renderCamera.transform.position - previewBounds.center ).magnitude + ( m_useLocalBounds ? ( previewBounds.extents.z * 1.01f ) : previewBounds.size.magnitude );
//activeRT = 현재 활성화 된 텍스쳐
RenderTexture activeRT = RenderTexture.active;
RenderTexture renderTexture = null;
try
{
//supersampledWidth = width * m_renderSupersampling 결과 값 반올림
//supersampledheight = height * m_renderSupersampling 결과 값 반올림
int supersampledWidth = Mathf.RoundToInt( width * m_renderSupersampling );
int supersampledHeight = Mathf.RoundToInt( height * m_renderSupersampling );
//너비 : supersampledWidth, 높이 : supersampledHeight, 깊이 16의 임시 렌더 텍스처를 할당
renderTexture = RenderTexture.GetTemporary( supersampledWidth, supersampledHeight, 16 );
//현재 활성화 된 텍스처 = renderTexture
RenderTexture.active = renderTexture;
//만약 배경색의 알파 값이 1보다 작으면 현재 렌더링 버퍼를 지움
if ( m_backgroundColor.a < 1f )
GL.Clear( true, true, m_backgroundColor );
//대상 렌더링 텍스처를 renderTexture 로 할당
renderCamera.targetTexture = renderTexture;
//만약 shader 가 없는 경우 카메라를 렌더링
if( !shader )
renderCamera.Render();
//이외의 경우 셰이더 교체로 카메라를 렌더링
else
renderCamera.RenderWithShader( shader, replacementTag ?? string.Empty );
//대상 렌더링 텍스처 = null
renderCamera.targetTexture = null;
//만약 초본의 너비가 현재 너비와 맞지 않거나, 초본의 높이가 현재 높이와 맞지 않는 경우
if( supersampledWidth != width || supersampledHeight != height )
{
RenderTexture _renderTexture = null;
try
{
//현재 너비와 높이에 맞는 임시 렌더 텍스쳐 생성
_renderTexture = RenderTexture.GetTemporary( width, height, 16 );
//현재 활성화 된 텍스처 = _renderTexture
RenderTexture.active = _renderTexture;
//만약 배경색의 알파 값이 1보다 작으면 현재 렌더링 버퍼를 지움
if ( m_backgroundColor.a < 1f )
GL.Clear( true, true, m_backgroundColor );
//셰이더를 사용하여 텍스처의 픽셀 데이터를 렌더 텍스처로 복사
Graphics.Blit( renderTexture, _renderTexture );
}
finally
{
//만약 _renderTexture 가 존재한다면 GetTemporary 로 할당된 임시 텍스처를 해제
if ( _renderTexture )
{
RenderTexture.ReleaseTemporary( renderTexture );
renderTexture = _renderTexture;
}
}
}
#if UNITY_2018_2_OR_NEWER
//비동기 호출 이벤트가 없는 경우
if (asyncCallback != null)
{
//GPU 리소스에서 데이터(renderTexture)를 비동기적으로 검색
AsyncGPUReadback.Request(renderTexture, 0, m_backgroundColor.a < 1f ? TextureFormat.RGBA32 : TextureFormat.RGB24, (asyncResult) =>
{
try
{
//너비, 높이, 배경 색을 지정한 새로운 텍스처 생성
result = new Texture2D(width, height, m_backgroundColor.a < 1f ? TextureFormat.RGBA32 : TextureFormat.RGB24, false);
//요청에 오류가 발생한 경우 CPU 메모리의 텍스처에 대한 픽셀 데이터를 설정
if (!asyncResult.hasError)
result.LoadRawTextureData(asyncResult.GetData<byte>());
//이외의 경우
else
{
Debug.LogWarning("Async thumbnail request failed, falling back to conventional method");
//_activeRT = 현재 활성화 된 텍스처
RenderTexture _activeRT = RenderTexture.active;
try
{
//현재 활성화 된 텍스처를 renderTexture 로 할당
RenderTexture.active = renderTexture;
//GPU의 현재 활성 렌더 대상에서 픽셀 색상의 직사각형 영역을 복사 하고 이를 CPU의 위치에 있는 텍스처를 사용
result.ReadPixels(new Rect(0f, 0f, width, height), 0, 0, false);
}
finally
{
//현재 활성화 된 텍스처를 _activeRT 로 할당
RenderTexture.active = _activeRT;
}
}
//CPU 텍스처의 변경 사항을 GPU에 복사
result.Apply(false, m_markTextureNonReadable);
asyncCallback(result);
}
finally
{
//만약 renderTexture 가 존재할 경우 GetTemporary 로 할당된 임시 텍스처를 해제
if (renderTexture)
RenderTexture.ReleaseTemporary(renderTexture);
}
});
//비동기 작업이 시작되었는지 여부를 true 로 변경
asyncOperationStarted = true;
}
//이외의 경우
else
#endif
{
//너비, 높이, 배경 색을 지정한 새로운 텍스처 생성
result = new Texture2D( width, height, m_backgroundColor.a < 1f ? TextureFormat.RGBA32 : TextureFormat.RGB24, false );
//GPU의 현재 활성 렌더 대상에서 픽셀 색상의 직사각형 영역을 복사 하고 이를 CPU의 위치에 있는 텍스처를 사용
result.ReadPixels( new Rect( 0f, 0f, width, height ), 0, 0, false );
//CPU 텍스처의 변경 사항을 GPU에 복사
result.Apply( false, m_markTextureNonReadable );
}
}
finally
{
//현재 활성화 된 텍스처를 activeRT 로 할당
RenderTexture.active = activeRT;
//만약 renderTexture 가 존재하면
if( renderTexture )
{
#if UNITY_2018_2_OR_NEWER
//만약 비동기 작업 시작 여부가 true 라면
if ( !asyncOperationStarted )
#endif
{
//GetTemporary 로 할당된 임시 텍스처를 해제
RenderTexture.ReleaseTemporary( renderTexture );
}
}
}
}
catch( Exception e )
{
Debug.LogException( e );
}
finally
{
//만약 모델의 복제 여부가 true 라면 previewObject 를 즉시 파괴
if ( shouldCloneModel )
Object.DestroyImmediate( previewObject.gameObject );
//이외의 경우
else
{
//만약 활성화 여부가 false 라면 previewObject 를 비활성화
if( !wasActive )
previewObject.gameObject.SetActive( false );
//만약 정적 객체 존재 유무가 false 라면 previewObject 의 위치, 회전값을 초기화
if( !isStatic )
{
previewObject.position = prevPos;
previewObject.rotation = prevRot;
}
int index = 0;
SetLayerRecursively( previewObject, ref index );
}
//만약 renderCamera 가 m_previewRenderCamera 인 경우 카메라 설정을 RuntimePreviewGenerator 의 설정값으로 적용
if ( renderCamera == m_previewRenderCamera )
cameraSetup.ApplySetup( renderCamera );
}
#if UNITY_2018_2_OR_NEWER
if( !asyncOperationStarted && asyncCallback != null )
asyncCallback( null );
#endif
return result;
}
//대상 객체의 AABB 경계를 계산하는 메서드
//AABB - Axis Aligned Bounding Box : 똑같은 축의 방향으로 물체의 경계상자가 만들어짐
public static bool CalculateBounds( Transform target, bool shouldIgnoreParticleSystems, Quaternion cameraRotation, out Bounds bounds )
{
//renderersList 초기화
//target 의 자식중 Renderer Component 를 가진 자식을 renderersList 에 넣기
renderersList.Clear();
target.GetComponentsInChildren( renderersList );
//inverseCameraRotation = cameraRoation 의 역수값 반환
Quaternion inverseCameraRotation = Quaternion.Inverse( cameraRotation );
Vector3 localBoundsMin = new Vector3( float.MaxValue - 1f, float.MaxValue - 1f, float.MaxValue - 1f );
Vector3 localBoundsMax = new Vector3( float.MinValue + 1f, float.MinValue + 1f, float.MinValue + 1f );
bounds = new Bounds();
bool hasBounds = false;
for( int i = 0; i < renderersList.Count; i++ )
{
//만약 renderersList 의 i 번째 Renderer Component 가 비활성화 되어있으면 continue;
if( !renderersList[i].enabled )
continue;
//만약 파티클 시스템 무시여부가 true이고, renderersList 중에 ParticleSystemRenderer 가 있다면 continue;
if ( shouldIgnoreParticleSystems && renderersList[i] is ParticleSystemRenderer )
continue;
//Local 의 경계를 계산하는 로직: https://github.com/Unity-Technologies/UnityCsReference/blob/0355e09029fa1212b7f2e821f41565df8e8814c7/Editor/Mono/InternalEditorUtility.bindings.cs#L710
//만약 m_useLocalBounds 가 true 인 경우
if( m_useLocalBounds )
{
#if UNITY_2021_2_OR_NEWER
#endif
Bounds localBounds = renderersList[i].localBounds;
Transform transform = renderersList[i].transform;
localBoundsMinMax[0] = localBounds.min;
localBoundsMinMax[1] = localBounds.max;
//이 부분은 세 개의 중첩된 루프를 사용하여 주어진 로컬 바운딩 박스의 모든 꼭지점을 카메라의 역회전 및 변환을
//거쳐 전역 공간에서의 좌표로 변환, 이를 이용하여 전역 공간에서의 최소 및 최대 지점을 계산하는 로직입니다.
//각각 x, y, z 축에 대해 두 가지 경우를 반복함으로써 세 개의 루프를 통해 2 * 3 = 8가지 꼭지점의 조합을생성
for ( int x = 0; x < 2; x++ )
{
for( int y = 0; y < 2; y++ )
{
for( int z = 0; z < 2; z++ )
{
//각 꼭지점의 로컬 좌표를 localBoundsMinMax에서 가져와 새로운 Vector3로 만듭니다.
//이후, 이 좌표를 transform.TransformPoint를 사용하여 해당 객체의 변환을 적용하고,
//그 결과를 inverseCameraRotation으로 역회전시킵니다.
Vector3 point = inverseCameraRotation * transform.TransformPoint( new Vector3( localBoundsMinMax[x].x, localBoundsMinMax[y].y, localBoundsMinMax[z].z ) );
//각 꼭지점에 대한 전역 좌표를 사용하여 localBoundsMin 및 localBoundsMax를 갱신합니다.
//localBoundsMin = 바운딩 박스의 최소 꼭지점
//localBoundsMax = 최대 꼭지점
localBoundsMin = Vector3.Min( localBoundsMin, point );
localBoundsMax = Vector3.Max( localBoundsMax, point );
}
}
}
//bound 를 가졌는지에 대한 여부를 true 로 지정
hasBounds = true;
}
//만약 bounds 를 가져오지 못했다면
else if( !hasBounds )
{
//bounds 를 renderersList 의 i번째 bounds 로 지정
//bounds 를 가져오는 여부를 true 로 지정
bounds = renderersList[i].bounds;
hasBounds = true;
}
//그 이외의 경우
else
//renderersList i 번째의 bounds 를 포함하도록 경계를 늘리는 작업을 수행
bounds.Encapsulate( renderersList[i].bounds );
}
//만약 m_useLocalBounds 와 bounds 를 가졌는지의 여부가 true 이면
if( m_useLocalBounds && hasBounds )
//bounds 를 재정의
bounds = new Bounds( cameraRotation * ( ( localBoundsMin + localBoundsMax ) * 0.5f ), localBoundsMax - localBoundsMin );
//bounds 를 가졌는지의 여부를 반환
return hasBounds;
}
// 경계를 완벽하게 캡슐화하는 방식으로 카메라를 이동하는 메서드
public static void CalculateCameraPosition( Camera camera, Bounds bounds, float padding = 0f )
{
//cameraTR = camera 의 transform
Transform cameraTR = camera.transform;
//cameraDirection = 카메라의 방향
//aspect = 카메라의 가로,세로 비율
Vector3 cameraDirection = cameraTR.forward;
float aspect = camera.aspect;
//만약 padding 이 0이 아닌 경우
if( padding != 0f )
bounds.size *= 1f + padding * 2f; //양쪽 가장자리에 패딩이 적용되어 2를 곱한 값
//bound 의 Center, Extents, Size 를 설정
Vector3 boundsCenter = bounds.center;
Vector3 boundsExtents = bounds.extents;
Vector3 boundsSize = 2f * boundsExtents;
// 경계의 모서리 점을 계산하는 로직
//로컬 좌표계(local space)를 기준으로 한 경계 상자의 꼭지점을 계산
if ( m_useLocalBounds )
{
//로컬 좌표계를 나타내는 변환 행렬을 생성합니다.
//boundsCenter를 중심으로 하고 camera.transform.rotation으로 회전하며 크기는 Vector3.one로 설정
//꼭지점의 초기값을 boundsExtents로 설정합니다.
Matrix4x4 localBoundsMatrix = Matrix4x4.TRS( boundsCenter, camera.transform.rotation, Vector3.one );
Vector3 point = boundsExtents;
//각 꼭지점을 localBoundsMatrix.MultiplyPoint3x4 메서드를 사용하여 로컬 좌표계에 맞게 변환한 후 boundingBoxPoints 배열에 저장합니다.
//꼭지점의 좌표를 계산하는 방식은 각 꼭지점마다 point를 이동 또는 변형시키는 것입니다.
boundingBoxPoints[0] = localBoundsMatrix.MultiplyPoint3x4( point );
point.x -= boundsSize.x;
boundingBoxPoints[1] = localBoundsMatrix.MultiplyPoint3x4( point );
point.y -= boundsSize.y;
boundingBoxPoints[2] = localBoundsMatrix.MultiplyPoint3x4( point );
point.x += boundsSize.x;
boundingBoxPoints[3] = localBoundsMatrix.MultiplyPoint3x4( point );
point.z -= boundsSize.z;
boundingBoxPoints[4] = localBoundsMatrix.MultiplyPoint3x4( point );
point.x -= boundsSize.x;
boundingBoxPoints[5] = localBoundsMatrix.MultiplyPoint3x4( point );
point.y += boundsSize.y;
boundingBoxPoints[6] = localBoundsMatrix.MultiplyPoint3x4( point );
point.x += boundsSize.x;
boundingBoxPoints[7] = localBoundsMatrix.MultiplyPoint3x4( point );
}
//전역 좌표계(global space)를 기준으로 한 경계 상자의 꼭지점을 계산
else
{
//꼭지점의 초기값을 boundsCenter와 boundsExtents를 더한 값으로 설정
Vector3 point = boundsCenter + boundsExtents;
//이후 각 꼭지점을 point를 이동 또는 변형시키는 방식으로 계산하고, 계산된 꼭지점을 boundingBoxPoints 배열에 저장합니다.
boundingBoxPoints[0] = point;
point.x -= boundsSize.x;
boundingBoxPoints[1] = point;
point.y -= boundsSize.y;
boundingBoxPoints[2] = point;
point.x += boundsSize.x;
boundingBoxPoints[3] = point;
point.z -= boundsSize.z;
boundingBoxPoints[4] = point;
point.x -= boundsSize.x;
boundingBoxPoints[5] = point;
point.y += boundsSize.y;
boundingBoxPoints[6] = point;
point.x += boundsSize.x;
boundingBoxPoints[7] = point;
}
//만약 카메라의 직각 투영 여부가 true 인 경우
if( camera.orthographic )
{
//camera 의 position 은 boundsCenter 로 지정
cameraTR.position = boundsCenter;
//minX, minY를 양의 무한대로 지정
//maxX, maxY를 음의 무한대로 지정
float minX = float.PositiveInfinity, minY = float.PositiveInfinity;
float maxX = float.NegativeInfinity, maxY = float.NegativeInfinity;
//boundingBoxPoints 의 갯수 만큼 반복
for( int i = 0; i < boundingBoxPoints.Length; i++ )
{
//boundingBoxPoints의 i번째의 위치를 로컬 공간 기준으로 변환
Vector3 localPoint = cameraTR.InverseTransformPoint( boundingBoxPoints[i] );
if( localPoint.x < minX )
minX = localPoint.x;
if( localPoint.x > maxX )
maxX = localPoint.x;
if( localPoint.y < minY )
minY = localPoint.y;
if( localPoint.y > maxY )
maxY = localPoint.y;
}
//길이를 경계상자의 범위+1 값으로 지정
//직교 투영 카메라의 보기 볼륨을 지정
//카메라의 위치값 지정
float distance = boundsExtents.magnitude + 1f;
camera.orthographicSize = Mathf.Max( maxY - minY, ( maxX - minX ) / aspect ) * 0.5f;
cameraTR.position = boundsCenter - cameraDirection * distance;
}
//그 이외의 경우
else
{
Vector3 cameraUp = cameraTR.up, cameraRight = cameraTR.right;
//수직의 시야각 지정
//수평의 시야각 지정
float verticalFOV = camera.fieldOfView * 0.5f;
float horizontalFOV = Mathf.Atan( Mathf.Tan( verticalFOV * Mathf.Deg2Rad ) * aspect ) * Mathf.Rad2Deg;
Vector3 topFrustumPlaneNormal = Quaternion.AngleAxis(90f + verticalFOV, -cameraRight) * cameraDirection;
Vector3 bottomFrustumPlaneNormal = Quaternion.AngleAxis( 90f + verticalFOV, cameraRight ) * cameraDirection;
Vector3 rightFrustumPlaneNormal = Quaternion.AngleAxis( 90f + horizontalFOV, cameraUp ) * cameraDirection;
Vector3 leftFrustumPlaneNormal = Quaternion.AngleAxis( 90f + horizontalFOV, -cameraUp ) * cameraDirection;
// Credit for algorithm: https://stackoverflow.com/a/66113254/2373034
int leftmostPoint = -1, rightmostPoint = -1, topmostPoint = -1, bottommostPoint = -1;
//카메라의 평면을 사용하여 경계의 가장자리 점 찾기
for ( int i = 0; i < boundingBoxPoints.Length; i++ )
{
if( leftmostPoint < 0 && IsOutermostPointInDirection( i, leftFrustumPlaneNormal ) )
leftmostPoint = i;
if( rightmostPoint < 0 && IsOutermostPointInDirection( i, rightFrustumPlaneNormal ) )
rightmostPoint = i;
if( topmostPoint < 0 && IsOutermostPointInDirection( i, topFrustumPlaneNormal ) )
topmostPoint = i;
if( bottommostPoint < 0 && IsOutermostPointInDirection( i, bottomFrustumPlaneNormal ) )
bottommostPoint = i;
}
//점을 통과하는 각 모서리 점에 대한 평면을 작성
Ray horizontalIntersection = GetPlanesIntersection( new Plane( leftFrustumPlaneNormal, boundingBoxPoints[leftmostPoint] ), new Plane( rightFrustumPlaneNormal, boundingBoxPoints[rightmostPoint] ) );
Ray verticalIntersection = GetPlanesIntersection( new Plane( topFrustumPlaneNormal, boundingBoxPoints[topmostPoint] ), new Plane( bottomFrustumPlaneNormal, boundingBoxPoints[bottommostPoint] ) );
//수평 모서리 점의 평면과 수직 모서리 점의 평면의 교점선 찾기
//카메라를 수평 교차로를 따라 이동하면 경계는 항상 카메라 폭이 완벽하게 설정됩니다
Vector3 closestPoint1, closestPoint2;
FindClosestPointsOnTwoLines( horizontalIntersection, verticalIntersection, out closestPoint1, out closestPoint2 );
//이 두 선 사이의 가장 가까운 선분을 찾고 해당 선의 가장 먼 지점에 카메라를 놓습니다
cameraTR.position = Vector3.Dot( closestPoint1 - closestPoint2, cameraDirection ) < 0 ? closestPoint1 : closestPoint2;
}
}
//경계의 모든 점 중에서 주어진 점이 주어진 방향의 가장 바깥쪽 점인지 여부를 반환하는 메서드
private static bool IsOutermostPointInDirection( int pointIndex, Vector3 direction )
{
Vector3 point = boundingBoxPoints[pointIndex];
//boundingBoxPoints 갯수 만큼 반복
for( int i = 0; i < boundingBoxPoints.Length; i++ )
{
//만약 i 가 pointIndex 가 아니고, direction 과 boundingBoxPoints[i] - point의 각도가 0보다 큰 경우 false 를 반환
//현재의 점 point와 다른 점 boundingBoxPoints[i] 간의 방향과 주어진 direction 벡터 간의 내적을 계산합니다.
//이 내적 값이 0보다 크다면, 현재의 point가 boundingBoxPoints[i]에 비해 주어진 방향으로 더 멀리 위치해 있는 것
if ( i != pointIndex && Vector3.Dot( direction, boundingBoxPoints[i] - point ) > 0 )
return false;
}
//true 를 반환
return true;
}
// Credit: https://stackoverflow.com/a/32410473/2373034
// 두 평면의 교점 선을 반환하는 메서드
private static Ray GetPlanesIntersection( Plane p1, Plane p2 )
{
//p1의 법선벡터와 p2의 법선벡터를 교차하여, 수직 벡터를 얻는 작업
//det = p3Normal 의 제곱 길이
Vector3 p3Normal = Vector3.Cross( p1.normal, p2.normal );
float det = p3Normal.sqrMagnitude;
//수직인 벡터 p3Normal을 두 번째 평면의 법선 벡터와 교차한 벡터에 첫 번째 평면의 길이를 곱함
//수직인 벡터 p3Normal을 첫 번째 평면의 법선 벡터와 교차한 벡터에 두 번째 평면의 길이를 곱함
//위 두 벡터의 합을 p3Normal 의 제곱 길이로 나누어 교차 지점을 계산, 방향은 p3Normal로 설정합니다.
return new Ray( ( ( Vector3.Cross( p3Normal, p2.normal ) * p1.distance ) + ( Vector3.Cross( p1.normal, p3Normal ) * p2.distance ) ) / det, p3Normal );
}
// Credit: http://wiki.unity3d.com/index.php/3d_Math_functions
//두 선 사이의 가장 가까운 선분의 모서리 점을 반환하는 메서드
private static void FindClosestPointsOnTwoLines( Ray line1, Ray line2, out Vector3 closestPointLine1, out Vector3 closestPointLine2 )
{
//line1,2 방향 지정
Vector3 line1Direction = line1.direction;
Vector3 line2Direction = line2.direction;
float a = Vector3.Dot( line1Direction, line1Direction );//첫 번째 선의 방향 벡터의 제곱 길이를 계산하여 변수 a에 저장합니다.
float b = Vector3.Dot( line1Direction, line2Direction );//첫 번째 선의 방향 벡터와 두 번째 선의 방향 벡터의 내적을 계산하여 변수 b에 저장합니다.
float e = Vector3.Dot( line2Direction, line2Direction );//두 번째 선의 방향 벡터의 제곱 길이를 계산하여 변수 e에 저장합니다.
float d = a * e - b * b;
Vector3 r = line1.origin - line2.origin; //두 선의 원점 간의 벡터를 계산하여 변수 r에 저장합니다.
float c = Vector3.Dot( line1Direction, r ); //각 선의 방향 벡터와 r 벡터의 내적을 계산하여 c에 저장합니다.
float f = Vector3.Dot( line2Direction, r ); //각 선의 방향 벡터와 r 벡터의 내적을 계산하여 f에 저장합니다.
float s = ( b * f - c * e ) / d;
float t = ( a * f - c * b ) / d;
//계산된 s 및 t 값을 사용하여 각 선상에서 가장 가까운 점을 찾아내어 반환합니다.
closestPointLine1 = line1.origin + line1Direction * s;
closestPointLine2 = line2.origin + line2Direction * t;
}
//카메라를 설정하는 메서드
private static void SetupCamera()
{
//만약 m_previewRenderCamera 가 있다면
if( m_previewRenderCamera )
{
//GetSetup 메서드 호출
//m_previewRenderCamera 설정값을 RuntimePreviewGenerator 의 설정값으로 적용
cameraSetup.GetSetup( m_previewRenderCamera );
//모델을 렌더링하는 카메라를 m_previewRenderCamera 로 설정
//모델을 렌더링하는 카메라의 nearClipPlane 을 설정
//모델을 렌더링하는 카메라의 cullingMask 를 설정
renderCamera = m_previewRenderCamera;
renderCamera.nearClipPlane = 0.01f;
renderCamera.cullingMask = 1 << PREVIEW_LAYER;
}
//이외의 경우 모델을 렌더링하는 카메라를 InternalCamera 로 설정
else
renderCamera = InternalCamera;
//모델을 렌더링하는 카메라의 배경색, 직각 투영 여부, 카메라가 배경을 지우는 방법을 설정
renderCamera.backgroundColor = m_backgroundColor;
renderCamera.orthographic = m_orthographicMode;
renderCamera.clearFlags = m_backgroundColor.a < 1f ? CameraClearFlags.Depth : CameraClearFlags.Color;
}
//obj 혹은 obj 의 자식이 정적객체인지 확인하는 메서드
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;
}
///obj 와 obj 의 모든 자식의 layer를 PREVIEW_LAYER 로 설정하는 메서드
private static void SetLayerRecursively( Transform obj )
{
obj.gameObject.layer = PREVIEW_LAYER;
for( int i = 0; i < obj.childCount; i++ )
SetLayerRecursively( obj.GetChild( i ) );
}
//obj 와 obj 의 모든 자식의 layer를 layerList 에 추가하는 메서드
private static void GetLayerRecursively( Transform obj )
{
layersList.Add( obj.gameObject.layer );
for( int i = 0; i < obj.childCount; i++ )
GetLayerRecursively( obj.GetChild( i ) );
}
//obj 와 obj 의 모든 자식의 layer를 layerList[index++] 에 해당하는 layer 로 설정하는 메서드
private static void SetLayerRecursively( Transform obj, ref int index )
{
obj.gameObject.layer = layersList[index++];
for( int i = 0; i < obj.childCount; i++ )
SetLayerRecursively( obj.GetChild( i ), ref index );
}
}

View File

@@ -1,12 +0,0 @@
fileFormatVersion: 2
guid: 196fe0787d5271a4e86aa5db3dbbbc3d
timeCreated: 1563638851
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,78 @@
using UnityEngine;
public static class ThumbnailGenerator
{
private static Vector3 offScreenPosition = new Vector3(-1000f, -1000f, -1000f);
public static Texture2D Generate(GameObject prefab, int width, int height)
{
if (prefab == null)
{
Debug.LogError("프리팹이 null");
return null;
}
GameObject studio = new GameObject("ThumbnailStudio");
studio.transform.position = offScreenPosition;
Light light = new GameObject("ThumbnailLight").AddComponent<Light>();
light.transform.SetParent(studio.transform);
light.type = LightType.Directional;
light.transform.rotation = Quaternion.Euler(50f, -30f, 0);
Camera camera = new GameObject("ThumbnailCamera").AddComponent<Camera>();
camera.transform.SetParent(studio.transform);
camera.clearFlags = CameraClearFlags.SolidColor;
camera.backgroundColor = Color.white;
camera.cullingMask = 1 << prefab.layer;
GameObject instance = Object.Instantiate(prefab, studio.transform);
Bounds bounds = GetTotalBounds(instance.transform);
PositionCamera(camera, bounds);
RenderTexture rt = RenderTexture.GetTemporary(width, height, 24);
camera.targetTexture = rt;
camera.Render();
RenderTexture.active = rt;
Texture2D thumbnail = new Texture2D(width, height, TextureFormat.RGBA32, false);
thumbnail.ReadPixels(new Rect(0, 0, width, height), 0, 0);
thumbnail.Apply();
RenderTexture.active = null;
camera.targetTexture = null;
RenderTexture.ReleaseTemporary(rt);
Object.DestroyImmediate(studio);
return thumbnail;
}
/// <summary>
/// 자식들을 포함한 전체 Bounds 계산
/// </summary>
private static Bounds GetTotalBounds(Transform target)
{
Bounds bounds = new Bounds(target.position, Vector3.zero);
Renderer[] renderers = target.GetComponentsInChildren<Renderer>();
foreach (Renderer renderer in renderers)
{
bounds.Encapsulate(renderer.bounds);
}
return bounds;
}
/// <summary>
/// Bounds에 맞춰 카메라의 위치와 줌(FOV) 조절
/// </summary>
private static void PositionCamera(Camera camera, Bounds bounds)
{
float maxExtent = Mathf.Max(bounds.extents.x, bounds.extents.y, bounds.extents.z);
float distance = maxExtent / Mathf.Sin(camera.fieldOfView * 0.5f * Mathf.Deg2Rad) * 1.2f; // 여백(1.2f) 추가
camera.transform.position = bounds.center - camera.transform.forward * distance;
camera.transform.LookAt(bounds.center);
camera.nearClipPlane = distance - maxExtent;
camera.farClipPlane = distance + maxExtent;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d1a33bd1046b91342b09591b2d621cbb