1편에서는 Probuilder를 이용한 고정된 형태의 입체도형를 보여주는 것에서 시작하여, 도형의 Property를 이용해 어떠한 형태의 입체도형이든 그려내기까지의 삽질기를 다뤘다.
이 포스팅에서는, 입체도형을 전개도로 펼치는 방법과 한 입체도형에서 가능한 모든 형태의 전개도를 찾는 방법에 대해 다룬다.
입체도형을 전개도로 펼치기
어떻게 입체도형을 전개도로 만들 수 있을 것인가?
먼저, 입체도형을 전개도로 만드는 행위를 분석해보면 다음의 사실을 발견할 수 있다.
- 우리는 n개의 면을 가진 입체도형에 대해 바닥면을 제외한 n-1번의 시행을 통해 전개도를 만든다.
- 면 x를 접기 위해서는 접고자 하는 방향에 닿아 있는 면 y에 '자식'으로 귀속됨을 의미한다.
- 면 x를 접거나 펴기 위해서는 각도 d가 필요하며, 각도 d는 이 두 행위에 공통적으로 적용된다.
- 각도 d는 면 x와 면 y의 벡터연산을 통해 구할 수 있다.
예외적으로, 면을 접을 수 없는 상황이 발생하는데 그 경우는 다음과 같다.
- 접고자 하는 면x을 기준으로, 접고자 하는 방향에 접하고 있는 면 y가 없는 경우
- 접고자 하는 면x가 이미 접혀 있는 경우
이러한 예외 사항을 판단하는 데에는 Fake cuboid를 사용하는데, 면 x의 특정 변 y의 중점에서 게임오브젝트를 하나 만들어 겹침 확인을 한 뒤 fake cuboid는 삭제하는 것이다. 코드는 다음과 같다.
public static Collider[] getOverlappingCollidersWithinCertainPosition(Vector3 centerPositionOfAxis){
GameObject fakeCuboid = new GameObject("faceCuboid");
fakeCuboid.transform.position = centerPositionOfAxis;
var rigid = fakeCuboid.AddComponent<Rigidbody>();
rigid.isKinematic = true;
rigid.useGravity = false;
var box = fakeCuboid.AddComponent<BoxCollider>();
box.size = Vector3.one * 0.1f;
box.isTrigger = true;
Collider[] colliders = Physics.OverlapBox(centerPositionOfAxis, box.size); //Plane이름을 가진 것만 추출
List<Collider> list = new List<Collider>();
for(int i=0; i<colliders.Length; i++){
if(colliders[i].transform.name.Contains("Plane")){
list.Add(colliders[i]);
}
}
Destroy(fakeCuboid);
return list.ToArray();
}
이런 식으로 예외 상황을 확인 뒤, 실제 특정 면을 특정 방향으로 접도록 하였다.
실제 활동은 접고자 하는 면 선택 -> 접을 수 있는 가능한 방향을 화살표로 표시 -> 화살표 클릭하면 해당 방향으로 접기로 구성된다. 이러한 활동은 미리 준비된 시나리오 활동이 아닌 알고리즘적 활동으로서, 학생들은 본인이 원하는 면을 본인이 원하는 방향으로 접어볼 수 있다.
가능한한 모든 전개도 형태 구하기
하지만, 입체도형을 전개도로 만드는 기능이 본 프로그램에 충분조건인 것은 아니다. 입체도형을 전개도로 만드는 기능과 전개도를 입체도형으로 만드는 기능은 완전 다른 이야기로, 위에서 개발한 알고리즘을 이용해 전개도를 입체도형으로 만들 수는 없다. 위 알고리즘은 단순히 학습자의 접는 활동을 모방한 것에 불과하다.
전개도를 입체도형으로 만들기 위해서는, 먼저 입체도형이 전개도가 되는 과정에 대한 데이터가 필요하다.
이러한 데이터는 "A0 B1 C2 D3"와 같이 표현될 수 있는데, 면 A를 0번 방향으로 접고 나서 면 B를 1번 방향으로 접고, 면 C를 2번 방향으로 접고 면 D를 3번 방향으로 접는 행위로 설명할 수 있다.
예를 들어, 피라미드 모양의 삼각뿔을 생각해보면, 밑면을 제외한 면의 수는 4개이고, 각 면에서 가능한 접는 방향의 수(변의 수)는 3개씩이며, 전개도를 만들기 위한 시행횟수는 4회이다.
따라서 이 입체도형에서 가능한 모든 경우의 접는 행위는
A0 B0 C0 D0
A0 B0 C0 D1
A0 B0 C0 D2
.......
와 같이 나타낼 수 있고, 이러한 경우의 수는 총 81가지가 나온다.
하지만, 이게 끝이 아닌데,
면을 선택하는 순서가 ABCD가 아닌, ABDC, DCBA 등 다양할 수 있다는 것이다.
따라서, 이를 정리하면 바닥면을 제외한 입체도형의 면의 수가 n개이고, 각각의 면이 갖고 있는 변의 수가 a,b,c...일 때, 가능한한 모든 경우의 수는 n! * (a*b*c*...)로 나타낼 수 있다.
이제 Test 클래스를 만들고, 기계의 힘을 이용해 한 입체도형이 만들어낼 수 있는 모든 전개도의 가능한 형태를 구해보자. 먼저, 입체도형의 면을 선택하는 순서에 대한 순열 배열을 하나 만들자. 그리고 이 순열 배열을 입력받은 코루틴에서 각 면의 순서에 맞게 특정 방향으로 면을 접도록 한다.
그러나, 이 n!의 값이 너무 크므로 멀티스레딩 유사 기능을 만들기 위해, 전개도를 찾을 오브젝트를 여러개 만들어 놓고, 각각의 오브젝트에 공평하게 순열 배열을 나눠주도록 하자. 코드는 다음과 같다.
IEnumerator startComputingAllAnswers(int imitationNumber){
GameObject testParent = new GameObject("Test Parent");
Utils.Permutation permutation = new Utils.Permutation( figureGameObject.GetComponent<FigureManager>().mainFigure.facesList.Count -1 ); //바닥면은 제외해야 하므로 면 수에서 -1개 함
List<string> permutationStr = new List<String>(); //0 1 2 , 0 1 2 와 같은 순열 배열이 str형태로 들어 있음
while(permutation != null){
permutationStr.Add(permutation.ToString());
permutation = permutation.Successor();
}
if(permutationStr.Count % imitationNumber != 0) //위의 go[]에 공평히 담을 수 없으면 작동 안함
{
Debug.Log("size error "+permutationStr.Count);
yield break;
}
GameObject[] imitatedFigures = new GameObject[imitationNumber]; //imitationNumber에 맞게 기존 도형과 arrows를 복제한다
int zPostion = diff;
int xPosition = diff;
for(int i=0; i<imitationNumber; i++){
GameObject figure = Instantiate(figureGameObject, new Vector3(xPosition, yPosition, zPostion), Quaternion.identity);
yield return StartCoroutine(startInitializing_FigureGO(figure));
figure.name = "FigureClone: "+(i+1);
imitatedFigures[i] = figure;
xPosition = xPosition + diff;
}
List<string> tempPermutationStr = new List<string>(); //imitaionNumber개씩 묶어서 코루틴 시작
int share = permutationStr.Count / imitationNumber; //예를 들어 24개를 6개씩 담으니 몫은 4임
for(int i =0; i<permutationStr.Count; i++){
if(tempPermutationStr.Count < share){
tempPermutationStr.Add(permutationStr[i]);
}
if(tempPermutationStr.Count == share){ //list가 꽉차면 코루틴 실행 후 리셋. 예를 들어 몫이 4인데 4개가 담겼으면
int GOarrayNum = i / share; //총 24개이므로 0,1,2,3,4,5 이렇게 나올거임
string[] newPermutationStr = new string[share]; //새로운 배열 생성
tempPermutationStr.CopyTo(newPermutationStr);
StartCoroutine(makePermuationForFaceAndDoAutomation(testParent, newPermutationStr, imitatedFigures[GOarrayNum]));
tempPermutationStr.Clear();
yield return new WaitForSeconds(0.2f);
}
}
}
이제 순열 배열을 입력받은 코루틴에서 면을 하나씩 선택하며 특정 방향으로 선택된 면을 접기 시작하는데, 이러한 작동을 가장 기초적인 방법으로 나타내면 다음과 같이 표현할 수 있다.
//i는 face를 순회하는 번호를 의미한다
for(int a=0; a<3; a++){
for(int b=0; b<3; b++){
for(int c=0; c<3; c++){
for(int d=0; d<3; d++){
for(int i=0; i< figureFaceCount_ExceptBottomFace; i++){
figure.setCurrentFace( newFaces[i] );
List<int> faceNum = new List<int>();
faceNum.Add(d);
faceNum.Add(c);
faceNum.Add(b);
faceNum.Add(a);
yield return StartCoroutine(figure.selectArrow(i, faceNum, 0.5f));
그리고 이러한 기초적 방법으로도 잘 작동하며, 열심히 일하며 가능한 모든 전개도를 찾는 모습은 다음과 같다.
그러나, 이 방법은 크게 두 가지 문제가 있었다.
첫째, 유지보수가 어렵다.
둘째, 불필요한 시행이 계속된다.
여기서 중요한 것은 불필요한 시행이라는 것인데, 상기 언급한 알고리즘에서는 그냥 무식하게 모든 경우를 다 검사하지만 실제 그럴 필요가 없다. 예를 들어, 두번째 면을 클릭했을 때나 세번째 면을 클릭했을 때, 그 면이 접힐 수 있는 방향은 4개가 아니라 1개 또는 2개일 수가 있는 것이다.
그래서, 나는 Node를 사용하기로 하고 코드를 수정하게 되었다. 효율성을 높이기 위한 방법으로, 일단 모든 경우의 수에 대한 node tree를 만들어 놓고, 면 x를 클릭했을 때 알게되는 불가능한 방향에 대한 정보를 바탕으로 해당 terminal node들을 삭제하는 방식을 사용하였다. 이를 통해 약 40%의 시행 횟수 감소를 가져올 수 있었다.
자세한건 다음에...
'Developer > 3D Planar Nets' 카테고리의 다른 글
3D 전개도 앱 개발기-1 (1) | 2020.05.08 |
---|