816 lines
32 KiB
C#
816 lines
32 KiB
C#
//#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 );
|
|
}
|
|
} |