본 기능은 유니티 Asset Store의 'Runtime OBJ Importer' 에셋을 기반으로 하여 기존 스크립트를 분석하고, 많은 사용자들이 겪는 버그(텍스쳐가 입혀지지 않음)를 해결하여 의도한 내용대로 작동하도록 재구성하였습니다.
기존 에셋에서 이용하는 기능 | 새로 재구성하거나 추가한 기능 |
.obj file의 vertex를 해석하고 mesh로 구성함 | texture를 정상적으로 입힐 수 있음 |
.obj와 texture files를 모두 다운로드 받을때까지 기다리는 큐 | |
동시다발적으로 여러 파일을 다운받고, 텍스쳐를 적용 | |
받은 obj를 List<GameObect>에서 관리할 수 있음 |
//ObjFromStream.cs
//Runtime OBJ Importer의 에셋 제공자인 Dummiesman의 네임스페이스 사용
using Dummiesman;
using System.IO;
using UnityEngine;
using System.Collections;
using UnityEngine.Networking;
//List 사용을 위해
using System.Collections.Generic;
public class ObjFromStream : MonoBehaviour
{
//CONSTS
//URL 주소
const string URL_PATH = "//자체 서버의 주소를 사용//";
//현재 적용하는 텍스쳐 개수
//[0] 텍스쳐맵
//[1] 노멀맵
//[2] 어클루전
const int APPLY_TEXTURES_CNT = 3;
//다운로드가 완료되었는지 확인하는 코루틴의 딜레이 초
WaitForSeconds DELAYTIME = new WaitForSeconds(1f);
//현재 진행중인 아이템 세트(obj + textures)의 남은 개수
private static uint mQueueLength = 0;
//아이템 세트 하나에서 필요한 모든 다운로드가 완료되었는지 확인하기위한 클래스 구조체
public class QueueChecker
{
//씬에 배치될 오브젝트
public GameObject instantiatedObj;
//텍스쳐 파일들
public Texture2D[] textures;
//텍스쳐 파일이 없어서 스킵하는지 확인
public bool[] isPassed;
//오브젝트 name을 서버에서 찾을 수 없는경우 다운로드 작업을 중단하기위해 확인
public bool isObjectNull;
//기본 생성자
public QueueChecker()
{
isPassed = new bool[APPLY_TEXTURES_CNT];
textures = new Texture2D[APPLY_TEXTURES_CNT];
isObjectNull = false;
for (int i = 0; i < isPassed.Length; ++i) { isPassed[i] = false; }
}
}
//스트림으로 받아온 오브젝트를 특정 부모의 자식으로 취급하기 위해 사용한다.
[SerializeField] private Transform[] mParentObj;
//스트림으로 받아온 오브젝트를 관리하기위한 리스트
private List<GameObject> mInstantiatedObj;
private void Start()
{
mInstantiatedObj = new List<GameObject>();
}
//해당 함수를 통해 다운로드를 시작한다.
public void StartDownloadFile(string objName, int parentID = -1)
{
++mQueueLength;
StartCoroutine(StartDownloadQueue(objName, parentID));
}
//다운로드를 시작한다.
private IEnumerator StartDownloadQueue(string objName, int parentID)
{
//새로 다운로드를 시작한다.
//queueData에서 다운로드 확인에 필요한 데이터들을 체크한다.
QueueChecker queueData = new QueueChecker();
//오브젝트 다운로드를 시작한다.
StartCoroutine(DownloadObj(objName, queueData));
//텍스쳐 다운로드를 시작한다.
for (int i = 0; i < APPLY_TEXTURES_CNT; ++i)
{
StartCoroutine(DownloadTexture(objName, queueData, i));
}
int iterator;
while (true)
{
if (queueData.isObjectNull)
{
Debug.Log("서버에서 " + objName + "을 찾을 수 없음");
--mQueueLength;
yield return DELAYTIME;
yield break;
}
//오브젝트 파일을 다운로드 받았는지 확인한다.
if (queueData.instantiatedObj == null)
{
Debug.Log("obj 다운로드 중..");
yield return DELAYTIME;
continue;
}
//텍스쳐 파일을 다운로드 받았는지 확인한다.
for (iterator = 0; iterator < APPLY_TEXTURES_CNT; ++iterator)
{
if(queueData.isPassed[iterator]) continue;
if (queueData.textures[iterator] == null)
{
Debug.Log(iterator + "번째 이미지 다운로드중..");
yield return DELAYTIME;
break;
}
}
if (iterator != APPLY_TEXTURES_CNT)
{
continue;
}
//여기에서 오브젝트를 활성화시키고, 텍스쳐를 입힌다.
{
//URP Lit Standard 쉐이더 코드 참조
//https://github.com/Unity-Technologies/MeasuredMaterialLibraryURP/blob/master/Assets/Measured%20Materials%20Library/ClearCoat/Shaders/Lit.shader
//SetFloat 함수 (메탈릭 스무스 값)
//https://forum.unity.com/threads/set-smoothness-of-material-in-script.381247/
Debug.Log("다운로드 모두 완료");
MeshRenderer renderer = queueData.instantiatedObj.GetComponent<MeshRenderer>();
//(가능)텍스쳐 변경
renderer.material.mainTexture = queueData.textures[0];
//(가능)노멀맵 변경
renderer.material.SetTexture("_BumpMap", queueData.textures[1]);
//(가능)어클루전맵 변경
renderer.material.SetTexture("_OcclusionMap", queueData.textures[2]);
//메탈릭 스무스 값 설정 (0~1)
renderer.material.SetFloat("_Smoothness", 0f);
//리스트에 해당 오브젝트를 관리하기위해 레퍼런스를 넣는다.
mInstantiatedObj.Add(queueData.instantiatedObj);
//만약 parentID가 -1이 아니라면 (의도하여 부모에게 넣으라고 했다면..) 해당 부모 오브젝트의 자식으로 처리한다.
if(parentID != -1)
{
queueData.instantiatedObj.transform.parent = mParentObj[parentID];
}
//오브젝트를 활성화 시킨다.
queueData.instantiatedObj.AddComponent<StreamActivator>();
queueData.instantiatedObj.SetActive(true);
--mQueueLength;
yield break;
}
}
}
//obj 파일을 다운로드 받는다. Dummiesman의 스크립트를 사용한다.
private IEnumerator DownloadObj(string objName, QueueChecker queueData)
{
string finalURL = URL_PATH + objName + ".obj";
using (UnityWebRequest www = UnityWebRequest.Get(finalURL))
{
yield return www.Send();
if (www.isNetworkError || www.isHttpError)
{
Debug.Log(www.error);
queueData.isObjectNull = true;
}
else
{
var textStream = new MemoryStream(www.downloadHandler.data);
queueData.instantiatedObj = new OBJLoader().Load(textStream);
queueData.instantiatedObj.name = objName;
}
}
}
//texture을 다운받아 QueueChecker에 넣는다.
private IEnumerator DownloadTexture(string objName, QueueChecker queueData, int imageID)
{
Debug.Log("텍스쳐 이미지 다운로드 시작");
string finalURL = URL_PATH + objName;
switch (imageID)
{
case 0:
{
finalURL += "_tex0.png";
break;
}
case 1:
{
finalURL += "_norm0.png";
break;
}
case 2:
{
finalURL += "_ao0.png";
break;
}
}
using (UnityWebRequest www = UnityWebRequest.Get(finalURL))
{
yield return www.Send();
if (www.isNetworkError || www.isHttpError)
{
Debug.Log(www.error);
queueData.isPassed[imageID] = true;
}
else
{
Texture2D tex = new Texture2D(2, 2);
tex.LoadImage(www.downloadHandler.data);
queueData.textures[imageID] = tex;
}
}
}
}
|
cs |
using Dummiesman;
- 기존 에셋의 제공자인 'Dummiesman'께서 구성한 네임스페이스를 사용합니다.
- 필자가 작성한 기능을 이용하기위해서는 에셋스토어에서 에셋을 다운받아야합니다.
const string URL_PATH = "..."
- .obj를 받아 사용하는 특수한 기능을 편리하게 이용하기위해 필자는 자체 서버에서 파일을 스트림하여 받기에 고정 주소 URL을 등록하여 사용하였습니다.
- URL_PATH 변수를 사용하지 않고, 함수를 호출할때 url을 직접 입력하여 사용할 수 있도록 할 수 있습니다.
const int APPLY_TEXTURES_CNT = 3;
- .obj에 텍스쳐를 입히기위해 사용하는 변수입니다. 현재 필자가 작성한 스크립트는 텍스쳐맵, 노멀맵, 어클루전맵. 총 3가지의 맵이 입혀지기에 3으로 설정하였습니다.
WaitForSeconds DELAYTIME = new WaitForSeconds(1f);
- 코루틴을 이용하여 파일을 다운받고, 모든 파일들이 다운로드 받아졌는지 확인하기 위해 검사를 합니다.
- 각 호출마다 새로운 WaitForSeconds를 선언하는것보다 하나의 멤버변수를 이용하여 사용하는것이 적절하다 판단하여 구성했습니다.
private static uint mQueueLength = 0;
- 단순히 현재 몇개의 데이터 세트가 다운로드 중인지 확인하기위해 사용하는 변수입니다.
public class QueueChecker { }
- 하나의 아이템 세트(.obj 파일과 texture 파일들)를 다운로드 받을 때, 해당 파일들이 모두 다운로드 받아졌는지 확인하기위한 변수들과 임시로 스트림한 데이터를 저장하기위해 사용하는 클래스 구조체입니다.
[SerializeField] private Transform[] mParentObj;
- 스트림으로 받은 .obj를 특정 부모의 자식으로 취급할 때, 해당 부모 오브젝트를 담는 트랜스폼 배열입니다.
private List<GameObject> mInstantiatedObj;
- 스트림으로 받은 .obj를 씬에 배치한 GameObject를 레퍼런스로 관리하기위한 리스트입니다.
public void StartDownloadFile(string objName, int parentID = -1) { }
- 외부에서 호출하며, 실제로 다운로드를 시작하고, 자동으로 특수한 행동을 하도록 하는 함수입니다.
- string objName을 통해 URL_PATH + objName으로 파일을 다운로드 받습니다.
- parentID가 -1이 아니라면, mParentObj[parentID]오브젝트의 자식으로 취급됩니다.
- 또한 objName을 구분하여 특수한 다운로드 후 배치되는경우 행동을 하도록 구현하였습니다.
private IEnumerator StartDownloadQueue(string objName, int parentID)
- 다운로드를 시작하고, 파일들이 다운로드 받아졌는지 확인하는 코루틴입니다.
- 모든 파일이 다운로드가 받아져 사용할 준비가 되어야 씬에 배치되도록 하였습니다.
- 그 이유는, 텍스쳐파일이 .obj보다 무거워 다운로드가 늦게된 상태에서 .obj가 먼저 씬에 배치되어버리면 텍스쳐가 없는 상태로 나올 수 있기때문입니다. 모든 준비가 완료되면 배치하도록 구현하였습니다.
- if (queueData.isObjectNull)는 URL에서 해당 .obj를 찾을 수 없는경우 다운로드를 취소하도록 합니다. .obj가 없으면 texture 파일들을 다운로드 받을 필요가 없어집니다.
- if (queueData.instantiatedObj == null) .obj파일이 다운로드받아져 'Dummiesman'에서 제공하는 기능을 이용해 GameObject로 변환되어 배치될 준비가 되었는지 확인합니다.
- for (iterator = 0; iterator < APPLY_TEXTURES_CNT; ++iterator) 모든 텍스쳐 파일들이 다운로드 되었는지 확인합니다.
- queueData.instantiatedObj.AddComponent<StreamActivator>(); 씬 내에 배치하기전에, StreamActivator라는 스크립트를 넣어줘 OnEnable()할때 특수한 기능을 수행할 수 있도록 구현하였습니다.
private IEnumerator DownloadObj(string objName, QueueChecker queueData)
- .obj 파일을 다운로드받고 'Dummiesman'에서 제공하는 기능을 이용하여 GameObject로 변환하여 queueData.instantiatedObj에 레퍼런스를 할당합니다.
- UnityWebRequest를 이용하여 url로부터 파일을 다운로드받고, 파일을 로컬에 저장하지않고 MemoryStream(www.downloadHandler.data);로 메모리에 적재하도록 합니다.
- OBJLoader().Load(textStream);은 'Dummiesman'에서 제공하는 기능입니다. Stream(.obj 데이터)를 각 태그별로 읽고, 이를 계산하여 GameObject로 만들어줍니다.
- 위 사진과같은 .obj format의 내부 값들을 읽어 정점의 위치나 다른 정보를 분석합니다.
private IEnumerator DownloadTexture(string objName, QueueChecker queueData, int imageID)
- .obj파일에 입힐 texture 이미지들을 다운로드 받기 위한 코루틴입니다.
- 현재 필자가 진행하는 프로젝트에서는 텍스쳐맵, 노멀맵, 어클루전 맵 3가지를 사용하기에 imageID(다운받는 이미지의 타입)에 맞는 파일을 다운로드받고, queueData.textures[imageID]에 레퍼런스를 넣어줘 텍스쳐를 입힐 수 있도록 준비 해줍니다.
- finalURL += "_tex0.png";와 같은 URL 작성은 자체 서버에서 사용하는 텍스쳐 이미지 파일의 이름 규칙을통해 나온 결과입니다.
.obj가 GameObject로 변환되고, 텍스쳐가 모두 준비되어 입혀지고 씬 내에 배치(인스턴스화)되면 mInstantiatedObj에서 관리하거나, 자체적으로 특수한 기능을 수행하도록 구현하였습니다. name과 case별 실행 스크립트는 예시입니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class StreamActivator : MonoBehaviour
{
void OnEnable()
{
switch(name)
{
case "baked_mesh":
{
transform.position = Vector3.left * 3.0f;
transform.localScale = Vector3.one * 3.0f;
break;
}
case "testa_mesh":
{
transform.position = Vector3.right * 3.0f;
transform.localScale = Vector3.one * 2.0f;
break;
}
}
Destroy(this);
}
}
|
cs |
void OnEnable()
- switch(name)으로 오브젝트의 이름을 구분하여 특정 행동을 취하도록 하는 스크립트입니다.
- case에서 또 다른 스크립트 추가하여 확장기능을 이용하도록, 무한한 확장 가능성을 제공합니다.
- Destroy(this);는 해당 스크립트는 OnEnable()을 실행하는 단발성 스크립트이기에, 역할을 수행하고 스스로 GameObject로부터 제거되도록 합니다.
StartDownloadFile("testa_mesh"); 호출로 다운로드되고 배치됩니다. 해당 obj는 텍스쳐 파일만 존재하는 obj로써 노멀맵과 어클루전맵은 다운로드가 스킵됩니다. 결국, .obj와 텍스쳐 파일 이미지만 다운로드되면 씬 내에 배치되고 StreamActivator 스크립트의 OnEnable이 실행되어 특수한 행동을 취하게 됩니다. 기본 매개변수로 호출하여 부모 오브젝트를 취급하지 않고, 독립적인 오브젝트로 씬에 배치됩니다.
StartDownloadFile("???"); 호출로 다운로드되고 배치되어야하지만, 현재 서버 내에 ???라는 이름의 obj는 존재하지 않기때문에 QueueChecker.isObjectNull = true가 되어 코루틴 내에서 필터링되어 다운로드가 중지됩니다. 이미지의 왼쪽에 1개의 다운로드 남음이 0으로 바뀐것을 볼 수 있습니다.
StartDownloadFile("baked_mesh", 0); 호출로 다운로드되고 배치됩니다. 모든 텍스쳐 유형이 존재하여 StartDownloadFile("testa_mesh");보다 시간이 걸립니다. 두번째 인자인 0은 -1(기본 매개변수)가 아니기에 mParentObj[0]오브젝트의 자식으로 취급되어 씬에 배치되어 Rotator(부모)가 회전함에따라 상대적으로 함께 회전하는것을 볼 수 있습니다.
하트 오브젝트를 다운로드받아 씬에 배치함과 동시에 특정한 활동을 수행하도록 한 이미지입니다. 다운로드를 많이 눌러도 정상적으로 작동하도록 구현하였고, 독립적으로 다운로드와, 다운로드가 완료되었는지 체크합니다. 이미지의 좌측에서 n개의 다운로드가 남았는지 체크도 가능합니다.
'unity game modules' 카테고리의 다른 글
[유니티] TTS(Text-To-Speech) 목소리 구현 (0) | 2022.08.05 |
---|---|
[유니티] 게임 옵션 저장 (0) | 2022.08.03 |
[유니티] 카메라 벽 통과 방지(카메라암) (0) | 2022.07.19 |
[유니티] 사운드를 편하게 관리하는 사운드매니저 (0) | 2022.07.19 |
[유니티] 대사 관리 및 다국어지원 (0) | 2022.07.18 |