← 과제 목록

[과제] 2주차) Basic-programming 노수연

@numkite
  • #과제
목차

개요

집에 있는 어항을 보고 그 모습을 만들어 보기로 했다. 배경, 모래, 바위, 수초와 물고기는 ShapeScene을 이용해 구성했다. 그리고 그냥 멈춰 있는 그림 보다 움직임이 있는 모습을 만들고 싶어 update()에서 물고기와 거품의 애니메이션도 추가해 보았다.

예제 구동 화면

Github: https://github.com/numkite/thorvg.example/blob/basic-programming/src/Aquarium.cpp

코드 설명

배경

어항 속 배경은 화면 전체를 덮는 사각형으로 만들었다. LinearGradient를 적용해서 아래쪽으로 갈수록 어두워지도록 수심에 따른 색 변화를 표현해 보았다.

auto background = tvg::Shape::gen();
background->appendRect(0, 0, w, h);

auto water = tvg::LinearGradient::gen();
water->linear(0, 0, 0, h);

tvg::Fill::ColorStop waterStops[3] = {
    {0.0f, 89, 195, 220, 255},
    {0.55f, 34, 137, 173, 255},
    {1.0f, 13, 72, 105, 255}
};

water->colorStops(waterStops, 3);
background->fill(water);
canvas->add(background);

물고기

물고기는 몸통, 꼬리, 지느러미, 눈을 각각 별도의 Shape로 만들고 하나의 Scene으로 묶었다. appendCircle()에 가로와 세로 반지름을 서로 다르게 전달해 타원형의 몸통을, moveTo()lineTo()로 점을 연결한 뒤 close()로 닫힌 도형을 만들어 꼬리를 표현했다.

auto body = tvg::Shape::gen();
body->appendCircle(0, 0, 52.0f * size, 30.0f * size);

auto tail = tvg::Shape::gen();
tail->moveTo(-45.0f * size, 0);
tail->lineTo(-82.0f * size, -30.0f * size);
tail->lineTo(-76.0f * size, 0);
tail->lineTo(-82.0f * size, 30.0f * size);
tail->close();

fish->add(body);
fish->add(tail);

Scene 전체에 translate()를 적용해 Scene으로 구성한 몸통과 꼬리 등을 각각 움직이지 않도록 했다. 그리고 색상과 크기를 매개변수로 전달해 같은 함수로 다른 두 마리의 물고기를 생성하도록 했다.

orangeFish = makeFish(245, 132, 76, 1.0f);
yellowFish = makeFish(248, 205, 86, 0.72f);

지형

수초와 모래, 바위는 직선으로만 표현하기 보다는 곡선이 들어가는 것이 더 자연스러울 것 같아 cubicTo()를 사용했다. 수초는 Cubic Bézier 곡선에 굵은 Stroke를 적용하여 만들었는데, StrokeCap::Round를 사용해 수초 끝부분이 둥글게 보이도록 했다.

auto weed = tvg::Shape::gen();
weed->moveTo(x, 560);
weed->cubicTo(
    x - 25, 520,
    x + 25, 490,
    x, 560 - height
);

weed->strokeFill(r, g, b);
weed->strokeWidth(10);
weed->strokeCap(tvg::StrokeCap::Round);

모래 바닥도 Cubic Bézier 곡선 두 개를 연결해 표현했다.

물고기 이동

물고기의 움직임은 update()에서 구현했다. elapsed를 초 단위로 변환한 뒤, fmodf()를 사용해 물고기가 화면 오른쪽을 벗어나면 다시 왼쪽에서 나타나도록 했다.

const auto seconds = elapsed * 0.001f;

auto orangeX = -90.0f + fmodf(seconds * 78.0f, 980.0f);
auto orangeY = 255.0f + sinf(seconds * 1.15f) * 12.0f;

orangeFish->translate(orangeX, orangeY);

물고기가 수평으로만 움직이는 것도 웃기지 않은가? 그래서 위아래로도 조금씩 흔들리면서 움직이도록 X 좌표는 시간에 따라 계속 증가하고, Y 좌표는 sinf()를 적용해 헤엄치는 듯한 움직임을 표현해 보려고 했다. 이 때 두 물고기의 움직임이 겹치지 않도록 하기 위해 서로 다른 속도와 위치 값을 사용했다.

거품

거품은 위치, 크기, 속도와 지연 시간을 서로 다르게 설정해 움직이도록 했다.

struct Bubble
{
    tvg::Shape* shape;
    float x;
    float startY;
    float speed;
    float delay;
};

거품 Shape는 content()에서 한 번만 생성하고, update()에서는 새로운 Shape를 만들지 않고 기존 Shape의 위치만 변경했다.

for (auto& bubble : bubbles) {
    auto rise = fmodf(
        (seconds + bubble.delay) * bubble.speed,
        470.0f
    );

    bubble.shape->translate(
        bubble.x,
        bubble.startY - rise
    );
}

거품이 일정 높이까지 올라가면 fmodf()에 의해 처음 위치로 돌아가도록 해 시간이 지나도 계속해서 거품이 배경으로 존재하도록 했다. 그리고 각 거품의 속도와 지연 시간이 다르게 해 여러 거품이 동시에 같은 위치에서 움직이지 않고 자연스럽게 나누어져 올라가도록 했다. 변경된 위치는 마지막에 canvas->update()를 호출해 화면에 반영했다.

사용한 ThorVG API

API사용 목적
Shape::gen()도형 생성
Scene::gen()물고기와 수초 그룹 구성
appendRect()물속 배경 생성
appendCircle()물고기 몸통, 눈, 거품과 작은 바위 생성
moveTo() / lineTo()물고기 꼬리와 바닥 Path 구성
cubicTo()지느러미, 수초, 모래와 바위의 곡선 표현
close()열린 Path를 닫힌 도형으로 변환
fill()도형 내부 색상 설정
strokeFill() / strokeWidth()수초와 거품 외곽선 설정
strokeCap()수초 끝부분을 둥글게 표현
LinearGradient물속 배경과 물고기의 색상 변화
Scene::add()여러 Shape를 하나의 Scene으로 그룹화
translate()물고기와 거품의 위치 변경
Canvas::update()위치 변경을 화면에 반영

댓글

Discussion 원문