컴퓨터 그래픽스/OpenGL

#10 Tinylender 개발일지 : 곡선으로 메시만들기

san10 2024. 9. 5. 11:54

저번에 만든 Pen 스크립트로 2D 메시를 만들 수 있다.

https://san10.tistory.com/59

 

#6 tinylender 개발일지 : 펜툴 제작

3D 모델을 만드는 방식은 여러가지가 있지만,tinylender는 일러스트레이터처럼 먼저 펜툴로 2D 도형을 그린 다음, 그걸 3D화 시킬 생각이다.  그래서 우선 2d 메시를 만들 수 있는 펜툴을 만들었다. 

san10.tistory.com

하지만 이 펜툴에 문제가 있다면

곡선을 그릴수는 없어서 곡선이 있는 메시는 만들수가 없다.

그래서 pen툴에 곡선을 그리는 기능을 추가했다.

 

 

베지어 스플라인

사실 곡선도 종류가 많다.

(베지어 곡선, 에르밋 곡선, 캣멀롬 스플라인, b-spline ....)

저번에 오디세이 지형을 개발을 하다가 곡선도 이렇게 종류가 많다는 걸 알게되었다..

 

곡선들마다 특징이 있고 자신의 프로젝트에 맞는 곡선을 선택하면 되는데..

나는 이 중에서 대중적인 많이 쓰이는 곡선인 베지어 스플라인으로 개발했다.

 

베지어 곡선은 제이점들을 보간해서 얻은 곡선인데,

베지어 스플라인은 베지어 곡선들의 첫점과 끝점을 대칭시켜

베지어 곡선을 연속적으로 이은 곡선이다.

 

어쨌든 보간을 해야하는데..

c++ 23 부터 lerp가 있다고 해서..

그냥 만들어서 썼다

template <typename T>
T lerp(T a, T b, T t)
{
    return a + t * (b - a);
}

 

 

 

조작법

조작 방식은 일러스트레이터의 펜툴과 유사하게 만들었다.

일러스트레이터에서 펜툴의 조작법은..

왼쪽 버튼을 누른 상태로 유지하면 이전에 눌렀던 점을 첫번째 제어점,

방금 눌려진 점을 마지막 제어점으로 하고

현재 누르고 있는 점을 두번째 제어점으로 하는 2차 베지어 곡선을 그린다.

 

그리고 이전에 곡선을 그렸다면

두가지 분기로 나뉘는데,

첫번째로 이전에 곡선을 그렸고 아무 버튼도 누르지 않은 상태라면,

이전에 눌렀던 점을 첫번째 제어점으로 하고

대칭이 되는 점을 구해서 그 지점을 두번째 제어점으로 하고

현재 마우스 커서가 있는 지점을 세번째 제어점으로 하는 2차 베지어 곡선을 그린다.

그리고 일반적인 상태(직선을 그리는 상태)로 돌아간다.

 

두번째는 이전에 곡선을 그렸고 왼쪽 버튼을 누르고 있는 상태라면

이전에 눌렀던 점을 첫번째 제어점으로 하고

대칭이 되는 점을 구해서 그 지점을 두번째 제어점으로 하고

방금 누른 지점을 마지막 제어점으로 하고

현재 누르는 지점을 세번째 제어점으로 하는 3차 베지어 곡선을 그린다.

그리고 곡선을 그렸던 상태로 돌아간다.

 

상태도

생각보다 상태가 복잡해서 상태패턴으로 분리할까 생각도 했었는데..

시간도 걸리고

상태가 더 추가될 것 같지도 않고

굳이 이걸 파편화하고싶지 않아서 하지는 않았다.

(사실 코드가 if문 떡칠이라 분리하기 쉽지않았음)

 

베지어 곡선

std::vector<glm::vec3> Pen::bezierSpline(glm::vec3 point1, glm::vec3 point2, glm::vec3 point3, int pointNum)
{
    std::vector<glm::vec3> bezierPoint;
    bezierPoint.reserve(pointNum);

    float space = 1.0f / static_cast<float>(pointNum);
    for (int i = 0; i < pointNum; i++)
    {
        float ax = lerp(point1.x, point2.x, space * i);
        float ay = lerp(point1.y, point2.y, space * i);

        float bx = lerp(point2.x, point3.x, space * i);
        float by = lerp(point2.y, point3.y, space * i);

        float x = lerp(ax, bx, space * i);
        float y = lerp(ay, by, space * i);
        bezierPoint.push_back(glm::vec3(x, y, 0.0f));
    }

    return bezierPoint;
}

std::vector<glm::vec3> Pen::bezierSpline(glm::vec3 point1, glm::vec3 point2, glm::vec3 point3, glm::vec3 point4, int pointNum)
{
    std::vector<glm::vec3> bezierPoint;
    bezierPoint.reserve(pointNum);

    float space = 1.0f / static_cast<float>(pointNum);
    for (int i = 0; i < pointNum; i++)
    {
        float ax = lerp(point1.x, point2.x, space * i);
        float ay = lerp(point1.y, point2.y, space * i);

        float bx = lerp(point2.x, point3.x, space * i);
        float by = lerp(point2.y, point3.y, space * i);

        float cx = lerp(point3.x, point4.x, space * i);
        float cy = lerp(point3.y, point4.y, space * i);

        float dx = lerp(ax, bx, space * i);
        float dy = lerp(ay, by, space * i);

        float ex = lerp(bx, cx, space * i);
        float ey = lerp(by, cy, space * i);

        float x = lerp(dx, ex, space * i);
        float y = lerp(dy, ey, space * i);

        bezierPoint.push_back(glm::vec3(x, y, 0.0f));
    }

    return bezierPoint;
}

베지어 스플라인을 만드려면..

베지어 곡선부터 만들어야 겠지..?

 

if (bPressed)
{
	if (glm::length(mNowPoint - point) < CURVE_DISTANCE)
	{
    	bCurve = false;
    	return;
	}
    ...
}

내가 지금 마우스를 꾹 누른 상태인지

그렇지 않은 상태인지 어떻게 판별하냐 하면은

일정 거리 이상 마우스를 움직이면 그렇다고 판별하고

아니라면 바로 리턴시켰다.

 

if (bCurve3rd)
{
    std::vector<glm::vec3> bezierPoints = bezierSpline(mVertices[mVertices.size() - 1].Position, mControlPoint1, point, mNowPoint, 30);
    for (int i = 0; i < 30; i++)
    {
        mSplineVertices[9 * i] = mVertices[0].Position.x;
        mSplineVertices[9 * i + 1] = mVertices[0].Position.y;
        mSplineVertices[9 * i + 2] = mVertices[0].Position.z;

        mSplineVertices[9 * i + 3] = bezierPoints[i].x;
        mSplineVertices[9 * i + 4] = bezierPoints[i].y;
        mSplineVertices[9 * i + 5] = bezierPoints[i].z;

        mSplineVertices[9 * i + 6] = bezierPoints[i + 1].x;
        mSplineVertices[9 * i + 7] = bezierPoints[i + 1].y;
        mSplineVertices[9 * i + 8] = bezierPoints[i + 1].z;
    }
    mSplineVertices[267] = mVertices[0].Position.x;
    mSplineVertices[268] = mVertices[0].Position.y;
    mSplineVertices[269] = mVertices[0].Position.z;
}

베지어 스플라인을 얻고

그 스플라인을 바탕으로 메시를 만들어 넣는 부분인데..

여기서 골때리는 버그가 생겼었다.

 

기능 자체는 잘 동작하는데

이 기능을 추가한 이후

자꾸 예상치 못한 곳에서 exception이 발생했다.

 

exception이 너무 이상한 곳에서

간헐적으로 자주 발생해서

디버깅이 매우 힘들었는데..

 

알고 보니 배열 크기를 잘못 계산해서 

배열 너머까지 다른 값으로 채웠었고

그래서 간헐적인 exception이 나는것이였다..

(위의 코드는 수정된 상태)

 

지금까지 했던 언어(java,c#,python 등등..)는

배열 너머 채우려고 하면 꼭 에러로그가 떴어서

배열을 초과했을 거라는 생각을 하지못했다.

지금은 std::array라는 컨테이너를 알게되서

일반적인 정적 배열 대신 사용중이다.

 

 

실행 결과

 

포인트를 누른채로 드래그하면 곡선 메시가 생성되고, 

이후에도 c1의 연속성을 가진 베지어 스플라인을 만든다.

 

그리고 그 상태에서 드래그하면

3차 베지어 스플라인의 메시를 만든다.

 

 

이번 개발일지는 코드를 하나하나 설명하기보다는

그냥 기억에 남았던 것 위주로, 기록할 만한 것들로 적었다.

코드가 너무 if문 떡칠이기도 하고..

전체코드는 여기에..

https://github.com/KES123450/Tinylender/blob/master/src/Pen.cpp

 

Tinylender/src/Pen.cpp at master · KES123450/Tinylender

Tinylender is very very very tiny lender. Contribute to KES123450/Tinylender development by creating an account on GitHub.

github.com