//#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 renderersList = new List( 64 ); private static readonly List layersList = new List( 64 ); private static Camera m_internalCamera = null; private static Camera InternalCamera { get { if( m_internalCamera == null ) { m_internalCamera = new GameObject( "ModelPreviewGeneratorCamera" ).AddComponent(); 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 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()); //이외의 경우 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 ); } }