히농의 잡합다식

UCPC 2021 이미지 제작 후기 본문

프로그래밍

UCPC 2021 이미지 제작 후기

히노히에 2021. 7. 30. 11:29

올해에도 어김없이 UCPC 의 출제진으로 참가하게 되었습니다.

사실 작년이랑은 다르게 올해는 회사 일이 폭발적으로 바빠지게 되었고...

게다가 UCPC 이외에 다른 대회 검수까지 합쳐서 검수만 총 4 탕을 뛰고있는 요즘입니다.

그래서...조금 구현이 귀찮을 것 같다 싶은 문제는 손이 안가게 되었고,

사실상 제대로 된 문제 검수를 못하고 있다는 점이 개인적으로 아쉬운 상황입니다... ㅠㅠ

 

그럼에도 불구하고 이렇게 글을 쓰고있는 이유는, 이번 검수에서 제가 조금 특이한 일을 했기 때문입니다.

바로 "이미지 제작" 입니다.

 

왜 이미지 제작인데 "프로그래밍" 태그에 있는가 하면..

이미지를 "프로그래밍" 으로 제작했기 때문입니다.

키랏-☆

 

원래는 github에서 혼자 꽁냥꽁냥하던, 이미지 그리는 코드가 있었는데..

이걸 활용해서 몇 가지 이미지를 만들 수 있겠다 싶어서 만들어보았고..

다행히 아무도 태클을 안걸어서 무사히 실제 대회 문제까지 출품할 수 있었습니다.

 

https://github.com/hinohie/ImageGenerator/

 

GitHub - hinohie/ImageGenerator: Generate Image

Generate Image. Contribute to hinohie/ImageGenerator development by creating an account on GitHub.

github.com

 

사실 모든 문제의 이미지를 제작한 것도 아니고... 정리해놓고 보니

예선 문제 3개, 본선 문제 3개에 대한 이미지만 제작해서

이렇게 글을 적어도 되나 싶긴 하지만..

그래도 일단 정리는 해보고 가고 싶었습니다ㅋㅋ

 

 

제가 이미지를 제작한 문제는 다음과 같습니다.

예선 : 

 - 흔한 타일 색칠 문제

 - 말뚝

 - 항체 인식

본선 :

 - Make Different

 - 봉화대

 - 츠바메가에시

 

참고로 이 글에는 문제 풀이와 관련된 내용이 일절 언급되지 않습니다.

문제에 대한 후기나 해설은 다른 사람들이 다 잘 해줄거라 믿습니다.

키랏-☆

 


1. 흔한 타일 색칠 문제

 

가장 네모네모하고 타일 색만 정해지면 그대로 네모네모하게 그리면 되기 때문에

가장 평온하게 제작했던 그림입니다.

 

그림을 그리는 순서는 크게 다음과 같았습니다.

 - 타일 색칠 + 구멍 그리기

 - 내부 경계선 (흰색) 그리기

 - 외부 경계선 그리기

 

타일의 색은 적당히 입력으로 주고,

최종 이미지의 width/height 및 padding 의 값은 적당히 상수값으로 설정해 두었습니다.

 

구멍(떼어낸 타일)은 @ 으로 출력하고 있기 때문에, 최대한 둥글둥글한 이미지를 형상하고 싶었고...

그냥 @ 문자 자체를 입력으로 넣어버렸습니다ㅋㅋㅋ

 

외곽선은, 인접한 4 개의 방향을 보면서 색이 같으면 내부 경계선, 아니면 외부 경계선으로 판단했습니다.

그림을 그릴 때 z-order 를 고려하지 않고, 그냥 draw 함수가 호출되는 순서대로 그려지므로

내부 경계선이 외부 경계선보다 위에 그려지는 상황을 막기 위해서

내부 경계선을 먼저 다 그린 뒤, 외부 경계선을 그렸습니다.

 

아래는 외부 경계선을 그릴 때 사용한 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void draw_outter_border() {
    int i, j, k;
    int ddx[4= { 0,1,0,-1 };
    int ddy[4= { 1,0,-1,0 };
    int ddxz[4= { 1100 };
    int ddxw[4= { 1001 };
    int ddyz[4= { 0110 };
    int ddyw[4= { 1100 };
    lf dx = (width - padding * 2.0/ m;
    lf dy = (height - padding * 2.0/ n;
    for (i = 0; i < n; i++) {
        for (j = 0; j < m; j++) {
            lf cx = padding + dx * j;
            lf cy = padding + dy * i;
            {
                for (k = 0; k < 4; k++) {
                    int ti = i + ddx[k];
                    int tj = j + ddy[k];
                    if (ti >= 0 && ti < n && tj >= 0 && tj < m && a[ti][tj] == a[i][j]) continue;
 
                    img.draw_line(cx + dx * ddxz[k], cy + dy * ddyz[k], cx + dx * ddxw[k], cy + dy * ddyw[k], min(dx, dy) * border_scale, 0.10.10.1);
                }
            }
        }
    }
}
cs

인접한 cell 의 색상이 같으면 넘어가고, 다르면 검은 색의 선분을 그리는 모습을 볼 수 있습니다.

 

아래는 코드 전문입니다.

tromino.cpp
0.01MB
해당 코드로 제작된 이미지

여담으로, @ 문자를 넣기 전, 최초로 제작했던 이미지는 아래와 같았습니다.

SAN수치가 높아지는것 같다는 이유로 폐기되었습니다 ㅠㅠ

\(・ω・\)SAN치!(/・ω・)/핀치!

 

2. 말뚝

 

말뚝 문제는 사실 삼각형 그리기 기능을 테스트하기 위해서 그렸던 그림입니다.

draw_triangle() 함수 기능을 만들어놓고, 이를 활용할만한 문제를 찾고있었는데

그냥 별 의미 없이, "풀밭에 말뚝을 박아 넣는 이미지를 그리면 삼각형이 필요하지 않을까?" 라는 결론에 도달해서

풀밭 이미지를 만들었습니다.

 

삼각형으로 그린 풀밭

그렇다면 제일 중요한 것은 풀을 생성하는 로직인데요..

그냥 tn 개 만큼의 삼각형을 그리겠다! 고 선언한 뒤,

전체 이미지에서 1/tn 지점마다 초록색 삼각형을 그리도록 했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void build_grass() {
    grass.setsize(width, pilesy + pileunit * 0.5);
    int i, j, k;
    grass.clean(000);
    grass.mSamplingMode = SamplingModeType::SAMPLING_3x3;
 
    int tn = 100;
    lf gscale = (lf)width / tn;
    for (i = 0; i <= tn+2; i++) {
        lf sx = i * gscale;
        lf sy = grass.h * 0.5;
        grass.draw_triangle(
            sx + nextint(-gscale, gscale), sy - nextint(grass.h * 0.3, grass.h * 0.5),
            sx + nextint(gscale, 2*gscale), sy + nextint(grass.h * 0.5, grass.h * 0.8),
            sx - nextint(gscale, 2*gscale), sy + nextint(grass.h * 0.5, grass.h * 0.8),
            nextint(0.0100.0/ 255.0,
            nextint(150255/ 255.0,
            nextint(0.0100.0/ 255.0,
            nextint(50100/ 100.0
            );
    }
 
    for (i = 0; i < grass.h; i++) {
        for (j = 0; j < grass.w; j++) {
            int id = i * grass.w + j;
            if (grass.data[id * 4 + 1< 120) {
                int zz = grass.data[id * 4 + 1];
                grass.data[id * 4 + 3= zz * 2;
            }
        }
    }
}
cs

 

그리고 풀 색상에 alpha값을 줘서, 어느정도 입체감을 줄 예정이였습니다.

풀이 너무 많아서 누가 앞에 있고 누가 뒤에 있는지는 아무래도 상관없게 된 것 같습니다만...ㅋㅋ

 

그 다음으로 중요한 것이 말뚝인데...

말뚝은 코드로 그리는 것이 힘들 것 같아서 그냥 그림판으로 하나 그렸습니다.

원기둥모양 + 나이테 + 음영.. 말뚝에 넣고 싶었던 요소들을 열심히 때려박고, 단순화시킨 결과물입니다.

그림판으로 적당히 그린 말뚝. 의외로 고퀄?

 

아래는 코드 전문입니다.

piles.cpp
0.00MB
해당 코드로 제작된 이미지 1
해당 코드로 제작된 이미지 2

아쉽게도, 이 ImageGenerator 라이브러리에는 글자 기능이 없어서

"5" 라는 숫자가 써진 이미지를 읽어들여, 그것을 그대로 출력하는 형태로

"5 만큼의 힘을 들여서" 라는 상황을 묘사했습니다.

 

 

3. 항체 인식

 

개인적으로 가장 재밌게 그렸던 그림입니다.

 

원래 초기에는 밋밋한 사각형 격자에 숫자만 적혀있던 이미지였습니다.

그러다가 '백신에 의해서 데이터가 변화된다' 는 듯한 인상을 강하게 주기 위해서 격자마다 배경색을 주었습니다.

그리고 배경색만 단색으로 있는게 밋밋해서 이것저것 배경을 넣다보니...괴물이 되었습니다 ㅠㅠ

풀소유

 

이번 그림에도 그림판으로 직접 그린 것들이 몇 가지 있습니다.

그 중 하나는 카와이한 주사기이고,

다른 하나는 각 조직마다 기본적으로 가지는 배경이미지입니다.

jusagi.png
jojic.png

 

초창기에는 저 하얀 jojic.png 이미지에다가, 약간의 멍울을 추가해서... '생체 조직' 느낌이 나도록 만들고 싶었습니다.

그리고 그렇게 만들어진 모습이 아래의 그림입니다.

초창기 모습

이 초창기 모습에서도...항체가 스며드는 공간을 표현하기 위해서 외곽선을 두 가지 색상으로 칠한다던가,

내부 영역에 대각선으로 선을 그었다던가 하는 모습을 볼 수 있습니다.

 

 

그러다가 배경이 너무 생체스럽지 않은 것 같아서.. "생체 조직" 느낌이 나기 위해서 어떻게 해야할까 고민을 했습니다.

 

우선은 생체 조직 하면 떠오르는 '세포'. 그리고 그 세포에는 '세포핵' 이 있습니다.

뭐 미토콘드리아 같은 짜잘한 것들을 전부 뭉뜽그려서 적당히 "동그란 세포 핵" 으로 생각할 수 있고..

따라서 배경으로 동글동글한 세포핵들을 무작위로 배치시켜 보았습니다.

네..세포핵..

동글동글한 세포핵만으로는 조금 부족할 것 같아서 생체 조직의 또다른 요소를 고민해 보았습니다.

바로 '혈관'인데요.

혈관은 기본적으로 기다란 줄 형태인데, 그 모습이 일직선이 아니고 이리저리 꺾여있는 모습입니다.

마침 구현해놓았던 기능 중 하나인 draw_polygon 을 이용해서

길쭉길쭉한 다각형 여러개를 합친 모양으로 혈관을 묘사하였습니다.

네..혈관..

그렇게 세포핵 + 혈관 을 넣고, 마지막으로 가운데 숫자를 출력해야할 부분에 숫자가 잘 보이도록 투명한 흰 색 원을 추가로 배치해 놓았습니다.

콘크리트 아님

전체 코드는 아래와 같습니다.

paint_image_generator.cpp
0.01MB
최종 콩크리트...아니, 생체 조직의 모습

이렇게 탄생한 것이 개당 300KB 해서 총 1.3MB 를 넘기는 용량의 이미지가 되어서 여러분의 소중한 패킷을 훔쳐가게 될 뻔했습니다ㅋㅋ

물론 이대로는 용량이 너무 커서, 이미지 크기를 50%씩 줄이고 용량도 개당 25~30 KB 로 낮춘 버전이 최종 문제에 사용되었습니다.

용량이 너무 크면 서버에 부하가 생겨서 또 서버 터질까봐, 그래서 백준님 UCPC 개최 후기에 제 이름이 언급될까봐 무서워서 용량을 줄일 수 밖에 없었습니다.

 

4. Make Different

 

여기서부터는 UCPC 본선에 사용된 문제 이미지에 대한 내용입니다.

이 문제는 원형으로 스프링을 배치하고, 그 위에 로봇 그려서 적당히 뛰어다니는 느낌이 들도록 그리는게 핵심이였습니다.

스프링 배치에 더해서 점프도 묘사해야하기 때문에, 그림을 3차원 느낌이 되도록 만드는게 필요했습니다.

그리고 아쉽게도 제 라이브러리는 이미지 회전같은 기능이 없었습니다ㅠ

초창기 이미지. 이런 3D 효과를 만들고 싶었습니다!

이런 문제를 해결하기 위해 약간의 아이디어를 가미해 착시효과를 주었습니다.

이미지의 아래쪽을 좀 더 두껍게 만들어서 원근감을 가미하는 것이죠.

착시효과를 일으키는 원형

이렇게 만든 원을 세로로 찌그러트리면 맨 위의 그림과 같은 3D 착시효과가 발생합니다.

 

이제 이 위에 배치할 스프링과 로봇 이미지를 그려야할 차례입니다.

아쉽게도 로봇이나 스프링을 코드로 제작할 자신이 없었고...

그냥 그림판을 써서 그렸습니다.

귀요미 스프링 (그림판 제작)

빨간색과 파란색 스프링을 만들어야 하는데 어차피 코드상으로 색을 바꾸면 되서, 녹색깔 스프링 하나만 만들었습니다.

저렇게 2단으로 구성된 스프링을 어디서 봤는진 모르겠지만 (아마 과거에 플래이했던 수많은 게임 중 하나일겁니다.)

암튼 저런 모양의 스프링 이미지가 떠올라서 저렇게 그렸습니다.

귀요미 로봇 (그림판 제작)

사실 로봇 2개의 이미지를 따로 만들고 싶었는데.. 위 로봇보다 더 귀여운 결과물을 만들 자신이 없어서

두 번째 로봇은 그냥 좌우만 반전시켜서 만들었습니다.

변명을 해보자면, 두 로봇이 동일한 방향으로 동시에 이동하며, 두 로붓이 서로 구분되어야할 필요가 없으므로

통일감있는 디자인이 더 합리적이였습니다.

 

그 다음은 점프방향을 나타내는 화살표를 그릴 필요가 있습니다.

아쉽게도 제 코드는 곡선을 그리는 기능이 없어서

100개의 직선을 그려서 곡선인척 흉내를 내야했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int tn = 100;
for (k = 0; k < tn; k++) {
    lf s1 = (lf)k / tn;
    lf s2 = (lf)(k + 1/ tn;
    lf a1 = ang1 * (1 - s1) + ang2 * s1;
    lf a2 = ang2 * (1 - s2) + ang2 * s2;
    vec2 p1 = pos1 * (1 - s1) + pos2 * s1;
    vec2 p2 = pos1 * (1 - s2) + pos2 * s2;
 
    lf diff1 = (s1 - 0.5* (s1 - 0.5* 4.0 * (1.0 + 0.5 * (sin(a1) + 1.0* 0.5);
    lf diff2 = (s2 - 0.5* (s2 - 0.5* 4.0 * (1.0 + 0.5 * (sin(a2) + 1.0* 0.5);
    size = line_width * (1.0 + 0.5 * (sin(a1) + 1.0* 0.5);
 
    p1.y += diff1 * line_offset_out;
    p2.y += diff2 * line_offset_out;
 
    img.draw_line(p1.x, p1.y, p2.x, p2.y, size000);
}
cs

 

전체 이미지를 제작하는 코드는 아래와 같습니다.

different.cpp
0.01MB
꿀귀

참고로 전 이 문제를 풀지 못했습니다. ㅠㅜ

 

5. 봉화대

 

봉화대 문제는 '산', '봉화대', '마을', 그리고 높이가 점점 높아지도록 봉화대를 설치할 수 밖에 없는 제한조건...

이미지로 그리고싶은 것이 아주 많은 문제였습니다.

그리고 그 이미지를 아래처럼 아주 끔찍하게 망친 것이 바로 저입니다. 죄송합니다. 하하.

봉화대 문제에 사용된 이미지

우선은 산은 다각형을 그리는 로직을 잘 활용해서 녹색 다각형을 그렸습니다.

그리고 봉화대를 설치했을 때 영향을 받는 마을들의 영역을 나타내는 불투명한 사각형 표시자도 하나 그려줍니다.

 

자...그럼 이제 중요한 것은 마을(...?) 위에 봉화대를 세워야만 그림이 완성될 것입니다.

봉화대를 만들 기본 틀

위 이미지가 기본이 되는 봉화대 이미지입니다. 갑자기 보라색과 청록색이 나왔습니다.

위 이미지를 바로 사용하는 것이 아니라, 적절한 코드를 통해서 한번 가공을 해, 회색빛 봉화대를 만들 예정입니다.

자세히 보시면 마을마다 세워진 봉화대 벽돌들의 배치가 다 다를겁니다.

 

일단 저 흰색 영역들은 회색들로 채워서 벽돌들을 연출할 것이라고 예상할 수 있겠는데요...

가운데 칼라풀한 사각형이 수상해 보입니다.

 

저 사각형은 봉화대의 '구멍'을 연출할 예정임을 나타내는 표식자로,

보라색에 가까울수록 더 깊은 느낌을 연출하기 위해 만든 사각형입니다.

이렇게 해서 저 부분에 사각형 모양의 구멍이 있는것 처럼 착시효과를 내려는 것이 목적이였습니다.

위 이미지로 생성한 봉화대 이미지

이렇게 만들어진 봉화대를 마을(...?!?!) 위에 배치하면 끝입니다.

 

전체 코드는 아래와 같습니다.

beacon.cpp
0.02MB

참고로 초기 이미지는 다음과 같은 모습이였습니다.

SAN 이 너무 그로테스크하게 나와서

SAN수치가 너무 높다는 이유로 기각당했습니다. ㅠㅠ

\(・ω・\)SAN치!(/・ω・)/핀치!

 

 

6. 츠바메가에시

 

이미지 제작의 핵심. 제가 출제한 문제입니다.

 

우선은 2차원 평면에 무언가를 그리는 것은 이제 일도 아니니 초기 제비의 배치를 그려보았습니다.

적당히 제비를 그리고, 이 제비 이미지와 문자이미지, 직선과 원을 이용해서 초기 상황을 그릴 수 있었습니다.

우선 제비
2차원 평면상에 배치된 제비

그 다음은 세번의 칼질이 필요합니다.

이 문제의 이미지를 셋팅하기 전에 이미 풀이 슬라이드를 만들어놓은 상태였기 때문에,

X축과 평행한 검격은 파란색, Y축과 평행한 검격은 빨간색으로

풀이 슬라이드와 통일을 시키고 싶었습니다.

그렇게 처음 나왔던 이미지는 대략 다음과 같았습니다.

레이저 비이임

이 모습은 무언가 검격이라기보단 레이저빛이 지나간 흔적으로밖에 보이지 않습니다.

뭔가 검이 지나간 흔적을 나타내기 위해선 좀 더 날카롭게 그릴 필요가 있을 것 같습니다.

 

검격이 지나간 날카로운 이미지를 어떻게 표현할 것인가를 고민했고... 그 결과,

"장력이 있는 공기 사이로 무언가가 빠르게 지나가고, 그 흔적을 그린 그림" 으로 모델링을 했습니다.

뭐...쉽게 말해서, 커튼을 빠르게 젖혔을 때의 모습을 상상하고 그림을 그렸습니다.

서걱!

그렇게 1차적으로 탄생한 것이 위 그림입니다.

좀 더 최근에 발생한 이벤트일수록 공기 접촉면이 많아서 더 밝아지고,

시간이 오래 지난 구간일수록 공기 접촉면이 줄어들어서 더 어두워지는 느낌을 주고 싶었습니다.

 

이 이후에 여러가지 이것저것 개선점을 주어서 검격 이미지를 열심히 만들어 나갔습니다.

검격만 이미지로 따로 뽑아내면 아래와 같습니다.

검격으로 사용된 이미지

전체적인 검의 흐름은 다음과 같은 느낌으로 제작되었는데요...

1. 밑면이 아래에 있고, 꼭지점이 위쪽으로 향하는 삼각형을 여러 개 그린다.

2. 밑면이 위에 있고, 꼭지점이 아래쪽으로 향하는 삼각형을 여러 개 그린다.

3. 전체 검의 궤적을 관통하는 하나의 밝고 얇은 선을 추가한다.

여기서 1,2 에 해당되는 삼각형의 꼭지점은 오른쪽으로 갈 수록 중심에 가까워지도록 모델링 했습니다.

랜덤을 업애고 그린 이미지

칼이 지나간 흔적을 만든 코드에서 랜덤을 없애면 위와 같은 그림이 나옵니다.

 

사실 저렇게 빠르게 검이 지나갔는데 제비들이 가만히 있는게 이상해서

약간의 blur effect도 추가했는데요..

- 검이 지나간 중심선 기준으로 좌/우로 갈라질 것

- 검의 진행방향으로 제비가 이동하는 듯한 느낌을 줄 것

- 검격에 당한 시간이 오래됐을 수록 좌/우로 많이 갈라질 것

이렇게 3 가지 요구조건을 만족하도록 공간왜곡(?)효과를 주는 방법을 생각했는데

마땅히 떠오르는 방법이 없었고..

 

별 생각이나 설계없이 이것저것 손대다가 최종적으로 정착한 것이 현재의 코드입니다....만

아쉽게도 이 코드를 저는 설명할 수 없을 것 같습니다.

지금 제가 뭘 만들었는지 모르겠습니다. 이게 뭐죠?

이...이게뭐지?

 

 

그리고 저 blur 효과가 멀쩡한 제비도 같이 왜곡시켜버리고 있길래.. 어쩔 수 없이

검의 영향을 받지 않는 제비는 나중에 따로 그렸습니다.

아무튼

 

사용한 전체 코드는 아래와 같습니다.

main.cpp
0.01MB
슉. 슈슉. 슉. 시. 시간초과.

그렇게 탄생한 것이 공간을 가르면서 제비를 절단하는 pichulia 의 검격입니다.

샷킹-!


여담:

스키..가 될 뻔한 아미지

원래는 스키장 문제도 그림을 그릴 예정이였으나.. "흰 눈"을 표현하는 것이 너무나 어려웠어서 도중에 포기했습니다.

위 이미지는 구상했던 그림의 스케치입니다. 조금은 아쉬웠던 부분...

 

나중에 기회가 된다면 다른 문제에서도 이런 리소스작업을 해보고 싶은데

회사일이 미쳐돌아가는 요즘같은 시즌 + 검수 4탕을 뛰고있어 주말에도 쉬지 못하는 요즘에

이런 그림 그릴 짬을 낼 수 있을지 모르겠습니다ㅋㅋㅋ

과연 검수 후기를 또 쓸 수 있는 날이 올 것인가..두둥..

Comments