#2: Floating Doughnut

Ritalin·2021년 3월 11일

Preface

인터넷에서 재미 있는 코드를 찾았다. 도넛 형태의 오브젝트가 공중에서 회전하는 애니메이션을 터미널 창에 프로젝션 하는 프로그램인데, 윈도우 환경에서 제대로 동작하지 않기에 내가 살짝 수정을 해봤다.

Code

원본 코드는 다음과 같다.

             k;double sin()
         ,cos();main(){float A=
       0,B=0,i,j,z[1760];char b[
     1760];printf("\x1b[2J");for(;;
  ){memset(b,32,1760);memset(z,0,7040)
  ;for(j=0;6.28>j;j+=0.07)for(i=0;6.28
 >i;i+=0.02){float c=sin(i),d=cos(j),e=
 sin(A),f=sin(j),g=cos(A),h=d+2,D=1/(c*
 h*e+f*g+5),l=cos      (i),m=cos(B),n=s\
in(B),t=c*h*g-f*        e;int x=40+30*D*
(l*h*m-t*n),y=            12+15*D*(l*h*n
+t*m),o=x+80*y,          N=8*((f*e-c*d*g
 )*m-c*d*e-f*g-l        *d*n);if(22>y&&
 y>0&&x>0&&80>x&&D>z[o]){z[o]=D;;;b[o]=
 ".,-~:;=!*#$@"[N>0?N:0];}}/*#****!!-*/
  printf("\x1b[H");for(k=0;1761>k;k++)
   putchar(k%80?b[k]:10);A+=0.04;B+=
     0.02;}}/*****####*******!!=;:~
       ~::==!!!**********!!!==::-
         .,~~;;;========;;;:~-.
             ..,--------,*/

이 코드를 그나마 가독성이 좋게 풀어보면 다음과 같다.

k;
double sin(),cos();
main(){
    float A=0,B=0,i,j,z[1760];
    char b[1760];
    printf("\x1b[2J");
    for(;;){
        memset(b,32,1760);
        memset(z,0,7040);
        for(j=0;6.28>j;j+=0.07)
            for(i=0;6.28>i;i+=0.02){
                float c=sin(i),
                      d=cos(j),
                      e=sin(A),
                      f=sin(j),
                      g=cos(A),
                      h=d+2,
                      D=1/(c*h*e+f*g+5),
                      l=cos(i),
                      m=cos(B),
                      n=sin(B),
                      t=c*h*g-f*e;
                int x=40+30*D*(l*h*m-t*n),
                    y=12+15*D*(l*h*n+t*m),
                    o=x+80*y,
                    N=8*((f*e-c*d*g)*m-c*d*e-f*g-l*d*n);
                if(22>y&&y>0&&x>0&&80>x&&D>z[o]){
                    z[o]=D;
                    b[o]=".,-~:;=!*#$@"[N>0?N:0];
                }
            }
        printf("\x1b[H");
        for(k=0;1761>k;k++)
            putchar(k%80?b[k]:10);
        A+=0.04;
        B+=0.02;
    }
}

솔직히 인덴션 넣으면 이해 될 줄 알았다. 하지만 알고 보니 그냥 수학이었다. 이해는 포기하기로 했다.
이제 여기서 그나마 이해할 수 있는 코드를 얘기해보겠다.

printf("\x1b[2J");
printf("\x1b[H");

이 두 줄이 눈에 밟힌다. 여기서 \x1b는 이스케이프 시퀀스로, ESC키의 아스키 값이다. 첫 번째 줄의 코드는 화면을 비우는 역할을 하고, 두 번째 줄의 코드는 커서를 (0, 0) 위치로 옮기는 역할을 한다. 한 가지 문제점이 있다면, 리눅스 계열의 운영체제에서만 동작한다는 사실이다.

윈도우에서 커서를 마음대로 옮길 수 있는 방법이 있다. WinAPI를 활용하는 것이다. 나는 여기에 세 줄의 코드를 추가할 것이다.

#include <windows.h>
// STD_OUTPUT_HANDLE, CONSOLE_CURSOR_INFO, COORD 등의 키워드를 사용하기 위해 추가했다.

SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &(CONSOLE_CURSOR_INFO){1, 0});
// 커서를 안 보이게 만든다.

SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),(COORD){0,0});
//커서를 (0, 0)으로 옮긴다.

결과 값은 다음과 같다.

#include <windows.h>
k;
double sin(),cos();
main(){
    float A=0,B=0,i,j,z[1760];
    char b[1760];
    SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &(CONSOLE_CURSOR_INFO){1, 0});
    for(;;){
        memset(b,32,1760);
        memset(z,0,7040);
        for(j=0;6.28>j;j+=0.07)
            for(i=0;6.28>i;i+=0.02){
                float c=sin(i),
                      d=cos(j),
                      e=sin(A),
                      f=sin(j),
                      g=cos(A),
                      h=d+2,
                      D=1/(c*h*e+f*g+5),
                      l=cos(i),
                      m=cos(B),
                      n=sin(B),
                      t=c*h*g-f*e;
                int x=40+30*D*(l*h*m-t*n),
                    y=12+15*D*(l*h*n+t*m),
                    o=x+80*y,
                    N=8*((f*e-c*d*g)*m-c*d*e-f*g-l*d*n);
                if(22>y&&y>0&&x>0&&80>x&&D>z[o]){
                    z[o]=D;
                    b[o]=".,-~:;=!*#$@"[N>0?N:0];
                }
            }
        for(k=0;1761>k;k++)
            putchar(k%80?b[k]:10);
        A+=0.04;
        B+=0.02;
        SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),(COORD){0,0});
    }
}

이제 마지막으로 이 코드를 도넛 모양으로 바꾸면 된다.
솔직히 이게 제일 힘들었다. WinAPI는 이미 만든 코드 복붙하는 게 다였으니 말이다.

           #include <windows.h>
         k;double sin(),cos();main
   (){float A=0,B=0,i,j,z[1760];char b[
  1760];SetConsoleCursorInfo(GetStdHandle
 (STD_OUTPUT_HANDLE),&(CONSOLE_CURSOR_INFO)
 {1,0});for(;;){memset(b,32,1760);memset(z,
0,7040);for(j=0;6.28>j;j+=0.07)for(i=0;6.28
>i;i+=0.02){float c=sin(i),d=cos(j),e=sin(A)
,f=sin(j),g=cos(A)      ,h=d+2,D=1/(c*h*e+f*
g+5),l=cos(i),m=          cos(B),n=sin(B),t=
c*h*g-f*e;int x            =40+30*D*(l*h*m-t
*n),y=12+15*D*(l          *h*n+t*m),o=x+80*y
,N=8*((f*e-c*d*g)*       m-c*d*e-f*g-l*d*n);
if(22>y&&y>0&&x>0&&80>x&&D>z[o]){z[o]=D;;;b
 [o]=".,-~:;=!*#$@"[N>0?N:0];}}for(k=0;1761
   >k;k++)putchar(k%80?b[k]:10);A+=0.04;
     B+=0.02;SetConsoleCursorPosition(
       GetStdHandle(STD_OUTPUT_HANDLE
            ),(COORD){0,0});}}

Epilogue

위에서 말했다시피 코드를 도넛 모양으로 만드는 일이 제일 힘들었다. 코드 공백을 최소화 시킨 후 도넛 모양으로 다듬어야 하는데 이게 진짜 고역이었다. 심지어 system("cls");만 추가한 버전, 커서를 안 보이게 만드는 코드가 없는 버전도 만들었기에 코드를 도넛 모양으로 다듬는 짓을 3번이나 한 셈이다. 진짜 힘들었다. 공부할 것도 많은데 이런 걸 하고 있어야 했나 하는 후회도 살짝 들기도 했다. 그래도 재미 있었다. 코드 실행 결과는 직접 컴파일을 해서 확인해보기 바란다. 컴파일하기 귀찮다면 다음 유튜브 영상에서 실행 결과를 확인해보기 바란다. 이 코드의 원본을 퍼온 출처이다.
https://www.youtube.com/watch?v=DEqXNfs_HhY

여담

아마 왜 stdio.h 등의 입출력에 사용되는 기본적인 헤더 파일은 추가하지 않았는데 windows.h는 추가했는지 궁금해 하는 사람이 있을 것이다. 이것은 사실 일종의 숏코딩 꼼수다. GCC 컴파일러는 선언되지 않은 함수가 호출 되었을 경우, 표준 라이브러리 중에 해당 함수가 존재하면 그 함수를 사용하여 컴파일한다. 다른 컴파일러는 사용해보지 않아서 모르겠다. 아, C++ 컴파일러로 컴파일할 경우에는 불가능하다.

그러면 여기서 또 의문이 생길 것이다.

그렇다면 windows.h를 추가할 이유가 없지 않느냐?

나도 그럴 줄 알았다. 근데 아니더라. 헤더 파일 내에 선언된 함수는 끌고 올 수 있어도 헤더 파일 내에 선언된 변수나 키워드는 끌고 올 수 없었던 모양이다. STD_OUTPUT_HANDLE, CONSOLE_CURSOR_INFO, COORD를 사용한 부분에서 오류가 나는 것을 보고 유추할 수 있었다.

profile
Mi chiamo Ritalin Methylphenidate

0개의 댓글