[Rotation3DCube] 회전하는 3D큐브를 콘솔로 구현하기

이성훈·2022년 11월 6일
0

원본 유튜브영상 >> https://www.youtube.com/watch?v=p09i_hoFdd0
원본제작자Servet Gulnaroglu Project GitHub >> https://github.com/servetgulnaroglu/cube.c.git
이를 바탕으로 클론코딩한 프로젝트 >> https://gitlab.com/clonecoding/spinning-cube

원본영상과 소스코드를 바탕으로 클론코딩하며, 한줄한줄 코드를 가능한한 분석해보았습니다.
영상의 순서대로 포스팅을 시작해보겠습니다.


영상에서 가장먼저 살펴본것이 회전 행렬(https://en.wikipedia.org/wiki/Rotation_matrix#Basic_rotations)이었다. 이 영상은 회전행렬을 이용해서 3D큐브를 메모리상에 그리고 그것을 2D로 그려냈다.(이때 Z축은 평면상으로 압축되며. 이 Z축과의 거리를 표현하는 변수도 설정하였다.)

그중에서
이 부분을 다뤘는데, 아마 각각 x,y,z축을 나타내는 벡터방향을 세타각도만큼 회전했을때 벡터의 방향인것같다.. 정확히 이해는 하지 못했다.

따라서 이 3x3행렬을 가지고 3차원공간의 하나의 [i, j, k]1x3 크기의 벡터를 각 축의 방향으로 세타만큼 회전시 그 값을 계산하기위해 아래의 사이트를 참고했다.
https://www.symbolab.com/

이를통해 [i, j, k] X Rx X Ry Rz의 값을 계산해보았다.

이렇게 3개의 식이 나오는데 각각이 X방향으로의 계산값 Y방향, Z방향이다.

이를 삼각함수를 이용해 코드를 짜면 아래와 같다.

그리고 앞으로 필요한 변수들을 코딩하였다.

코드를 분석한결과 현재까지 알아낸바는 주석으로 처리해 두었다.

다음에는 큐브의 각 면에대해 위의 3개의함수를 이용해 zBuffer와 buffer를 계산하는 함수이다.
여기서 Z축과 떨어진거리를 나타내는 변수 distanceFromCam이 쓰인다.
저 값을 y에 더하고,
OutOfZone = 1 / y,
그리고 line53의 displayY대신 displayZ를 계산하면 캠의 축을 이동가능하다.
여튼 콘솔창에 출력하는 평면은 XY평면이니 displayX, displayY를 계산한다.
(계산식이 유도되는 자세한 과정은 >> https://github.com/servetgulnaroglu/cube.c/issues/2)
이때 line52, 53의 backgroundWidth / 2 를통해 화면의 정중앙을기준으로 삼게된다. (추후에 다시 설명하겠다.)

마지막으로 큐브를 그리는 main함수를 살펴보겠다.
중간부분은 또다른 큐브에대한 코드이므로 첫번쨰 큐브만 살펴보자.

  • line63, 94 : 콘솔창에서 하나의 프레임을 입력하기 앞 뒤 콘솔출력내용을 지우는 ANSI값이다.
  • line65, 66 : 하나의 프레임을 구성하는 두 버퍼를 초기화
  • line69 : 추후에 다시 설명하겠다는 그 부분이다. 큐브의 중심점위치를 지정하는 코드다. 이것은 마치
    이처럼 그 큐브의 중심점을 잡게된다
  • line72~77 : 이부분은 하나의 큐브면을 기준으로 앞서 사용한 https://www.symbolab.com/사이트에가서 x, y, z값을 얼마나 변환할지 계산해서 그만큼 calculateForSurface의 3개의 인자(float cubeX, float cubeY, float cubeZ)를 적는 코드다.
    한가지예로 line 73을 보면

이런 과정을 거쳐서 큐브가 정육면체이므로, 그 회전한 결과를 세 변수x, y, z의 재조합만으로 출력이 가능하다.
이때 x = cubeX, y = cubeY z = -cubeSize 이므로,
calculateForSurface(cubeSize, cubeY, cubeX, '문자')를 출력하면되는것이다 !
그래서 Ry를이용해 90도, -90, -90, 180도 회전,
Rx를 이용해 90도, -90도 회전하여 한프레임의 각 6면을 결정하면된다.
Rx를 이용하여 90도 회전시킨다면 아래와 같이 나온다.

  • line 95~96 : 콘솔창에 실제로 출력하는 부분이다. 버퍼를 출력한다.
  • line 100~102 : 마지막으로, 각 축방향으로 큐브를 회전시킬 속도를 결정한다. 이부분에서 한 축방향을 고정시키면, 위아래 또는 좌우로만 회전되는 큐브를 볼 수 있다.

이제 실행을 해보자.





누워서 유튜브보다가 20분짜리 짧은 영상으로 큐브를 구현하는 한 외국인의 영상을 보다가 너무 흥미로워서
바로 일어나서 클론코딩을 해보았다.
그 과정에서 회전행렬하나만을 이용해서 각 면의 위치를 계산해냈는데, 이 부분을 이해 하려면 수학적지식이 요구될것같다.
물론 이 방법말고도 quarternions 쿼터니언즈 라는것을 이용해도 가능하다고하는데, 이것이 더 복잡하다고 한다.
하나의면을 가지고 90도, -90도, 180도하여 3개의면을 더 표현하는것을보니, 어쩌면 코드를 조금더 수정하면 정사면체를 회전시킬 수 있지않을까 싶다.
이런 흥미로운 주제를 영상으로 남겨준 유튜버 Servet Gulnaroglu에게 다시한번 감사를 전한다.

작성한 소스코드를 올리며 마치겠다.

#define _CRT_SECURE_NO_WARNINGS 
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <windows.h>
/*
* 행렬의 회전을 이용한 3D 큐브를 2D공간에 출력하는 프로그램
* => 회전행렬이 아닌 quarternions를 이용해도 가능할것이다.(다만 기하, 대수학범위까지 확장됨)
* 
*/
float axisX, axisY, axisZ; //각 축의 위치

float cubeSize;//큐브의 실제 크기 너무 크면 큐브의 한면에 큐브의 뒷면이 출력되는 오류가남(float의 한계)
int backgroundWidth = 160, backgroundHeight = 44; //백그라운드 전체 크기(출력공간)
float zBuffer[160 * 44]; //깊이값
char buffer[160 * 44]; //출력할 문자열을 담는공간
int backgroundASCIICode = '.'; //빈공간에 출력할 문자(정수값)
int distanceFromCam = 100; //큐브를 실제로 바라보는 거리(큐브와의 Z축 거리)
float cubeCenterOffset; //시작위치
float incrementSpeed = 0.8f; //큐브마다 개별적으로 설정불가

float calculateX(int i, int j, int k) {
	return j * sin(axisX) * sin(axisY) * cos(axisZ)
		- k * cos(axisX) * sin(axisY) * cos(axisZ)
		+ j * cos(axisX) * sin(axisZ)
		+ k * sin(axisX) * sin(axisZ)
		+ i * cos(axisY) * cos(axisZ);
}

float calculateY(int i, int j, int k) {
	return j * cos(axisX) * cos(axisZ) + k * sin(axisX) * cos(axisZ)
		- j * sin(axisX) * sin(axisY) * sin(axisZ) + k * cos(axisX) * sin(axisY) * sin(axisZ)
		- i * cos(axisY) * sin(axisZ);
}

float calculateZ(int i, int j, int k) {
	return k * cos(axisX) * cos(axisY)
		- j * sin(axisX) * cos(axisY)
		+ i * sin(axisY);
}

//큐브의 각 면의모양을 계산 : zBuffer, buffer작성
void calculateForSurface(float cubeX, float cubeY, float cubeZ, int ch) {
	float x = calculateX(cubeX, cubeY, cubeZ);
	float y = calculateY(cubeX, cubeY, cubeZ);
	float z = calculateZ(cubeX, cubeY, cubeZ) + distanceFromCam; //큐브와 Z축거리만큼 떨어져서 봄

	float OutOfZone = 1 / z; // 2/z, 3/z할수록 큐브가 커진다..?
	float K1 = 40;

	//2D공간에 실제로 출력하는것은 X, Y좌표만임
	int displayX = (int)(backgroundWidth / 2 + cubeCenterOffset + K1 * OutOfZone * x * 2);
	int displayY = (int)(backgroundHeight / 2 + K1 * OutOfZone * y);

	int idx = displayX + displayY * backgroundWidth;
	if (idx >= 0 && idx < backgroundWidth * backgroundHeight && OutOfZone > zBuffer[idx]) {
		zBuffer[idx] = OutOfZone;
		buffer[idx] = ch;
	}
}

int main() {
	printf("%\x1b[2J"); //콘솔창의 윗쪽화면 지우기
	while (1) {
		memset(buffer, backgroundASCIICode, backgroundWidth * backgroundHeight);
		memset(zBuffer, 0, backgroundWidth * backgroundHeight * 4);
		//첫번째 큐브
		cubeSize = 23;
		cubeCenterOffset = -1.5 * cubeSize; //화면 정중앙으로부터 왼쪽으로 2 * cubeSize위치가 큐브의 중심
		for (float cubeX = -cubeSize; cubeX < cubeSize; cubeX += incrementSpeed) {
			for (float cubeY = -cubeSize; cubeY < cubeSize; cubeY += incrementSpeed) {
				calculateForSurface(cubeX, cubeY, -cubeSize, '1'); 
				calculateForSurface(cubeSize, cubeY, cubeX, '2'); //90도
				calculateForSurface(-cubeSize, cubeY, -cubeX, '3'); //-90도
				calculateForSurface(-cubeX, cubeY, cubeSize, '4'); //180도
				calculateForSurface(cubeX, -cubeSize, -cubeY, '5'); //90도
				calculateForSurface(cubeX, cubeSize, cubeY, '6'); //-90도
			}
		}

		//두번째 큐브
		cubeSize = 23;
		cubeCenterOffset = 1.5 * cubeSize;
		for (float cubeX = -cubeSize; cubeX < cubeSize; cubeX += incrementSpeed) {
			for (float cubeY = -cubeSize; cubeY < cubeSize; cubeY += incrementSpeed) {
				calculateForSurface(cubeX, cubeY, -cubeSize, '6');
				calculateForSurface(cubeSize, cubeY, cubeX, '5'); //90도
				calculateForSurface(-cubeSize, cubeY, -cubeX, '4'); //-90도
				calculateForSurface(-cubeX, cubeY, cubeSize, '3'); //180도
				calculateForSurface(cubeX, -cubeSize, -cubeY, '2'); //90도
				calculateForSurface(cubeX, cubeSize, cubeY, '1'); //-90도
			}
		}
		printf("\x1b[H"); //콘솔창의 아랫쪽화면 지우기
		for (int k = 0; k < backgroundWidth * backgroundHeight; k++) {
			putchar(k % backgroundWidth ? buffer[k] : 10);
		}

		//큐브의 각축의방향으로 회전 속도
		axisX += 0.08;
		axisY += 0.08;
		axisZ += 0.08;
		//Sleep(100);
	}

	return 0;
}
profile
I will be a socially developer

0개의 댓글