C\C++/OpenGL

C/C++ OpenGl을 이용한 간단한 태양계 구축 프로젝트(1)

sundori 2023. 11. 8. 22:33

Windows MFC (Microsoft Foundation Classes)를 사용하여 OpenGL을 초기화하고 간단한 2D 그래픽을 렌더링 하는 뷰 클래스를 사용하여 간단한 태양계 구축하기.

이번에는 OpenGL을 사용하여 태양계 모델링을 구현과 MFC(Microsoft Foundation Classes)를 사용하여 Windows 환경에서 OpenGL을 초기화하고 3D 모델을 렌더링 하여 보자.

 

COpenGLView 클래스

  1. COpenGLView 클래스 생성자 (COpenGLView::COpenGLView())
    클래스 생성자에서는 행성과 달의 회전 등 각도 초기화를 수행한다!
  2. COpenGLView::PreCreateWindow 함수
    윈도우를 생성하기 전에 윈도 클래스 및 스타일을 수정하며, OpenGL 관련 플래그를 추가하고 화면 스크린 리더링 중 화면 청소 영역을 설정합니다.
cs.style |= (WS_CLIPCHILDREN | WS_CLIPSIBLINGS | CS_OWNDC);

 

cs.style |= (WS_CLIPCHILDREN | WS_CLIPSIBLINGS | CS_OWNDC); 이 코드는 윈도를 생성할 때 사용되는 CREATESTRUCT 구조체의 style 멤버에 특정 스타일 플래그를 추가하는 것을 의미하며, OpenGL을 사용하는 경우에 주로 설정되며 다음과 같은 역할을 한다.

  1. WS_CLIPCHILDREN: 이 스타일은 윈도우의 자식 윈도들이 상위 윈도의 클라이언트 영역에서 그려질 때, 다른 자식 윈도들의 영역을 덮지 않도록 합니다. 이것은 자식 윈도들 간의 그리기 충돌을 방지하고 그림이 깨지는 것을 방지하는 데 도움이 된다.
  2. WS_CLIPSIBLINGS: 이 스타일은 동일한 레벨에 있는 (동등한 부모 윈도우를 가진) 다른 윈도들이 현재 윈도의 클라이언트 영역을 덮지 않도록 하며, 이것은 형제 윈도 간의 그리기 충돌을 방지한다.
  3. CS_OWNDC: 이 스타일은 윈도우가 자체 디바이스 콘텍스트 (DC)를 소유하고 사용한다는 것을 나타냅니다. OpenGL을 사용할 때, OpenGL 디바이스 콘텍스트를 소유하는 것이 중요하기에 CS_OWNDC 플래그를 설정하면 현재 윈도에 대한 OpenGL 콘텍스트를 만들고 설정하는 데 도움이 된다.

 

  1. OpenGLView::OnDraw 함수
    OpenGL을 사용하여 실제 그래픽을 그리는 함수입니다. 현재는 비어 있으며, 여기에 원하는 그래픽 렌더링 코드를 추가한다.
  2. COpenGLView::OnRButtonUp 함수
    마우스 오른쪽 버튼 업 이벤트를 처리하는 함수로, 컨텍스트 메뉴를 표시하기 위한 로직 등을 넣는다.
  3. OpenGL 초기화 및 설정
    COpenGLView::OnCreate 함수
    • OpenGL 컨텍스트 생성
    • 픽셀 형식 설정
    • 렌더링 컨텍스트를 현재로 만들기
    • SetupRC() 함수를 호출하여 초기화 설정 수행
    • 타이머 설정 (행성과 달의 회전을 위한 타이머)
    1. 윈도가 처음 생성될 때 호출되는 함수로, OpenGL 초기화를 수행한다!
int nPixelFormat;
m_hDC = ::GetDC(m_hWnd);

static PIXELFORMATDESCRIPTOR pfd = {
    sizeof(PIXELFORMATDESCRIPTOR),
    1,
    PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
    PFD_TYPE_RGBA,
    24,
    0, 0, 0, 0, 0, 0,
    0, 0,
    0, 0, 0, 0, 0,
    32,
    0,
    0,
    PFD_MAIN_PLANE,
    0,
    0, 0, 0
};
nPixelFormat = ChoosePixelFormat(m_hDC, &pfd);
VERIFY(SetPixelFormat(m_hDC, nPixelFormat, &pfd));
m_hRC = wglCreateContext(m_hDC);
VERIFY(wglMakeCurrent(m_hDC, m_hRC));
  
SetupRC();

SetTimer(1, 50, NULL);  // 행성 회전 타이머
SetTimer(2, 17, NULL);  // 달 회전 타이머
  1. COpenGLView::OnDestroy 함수
    윈도우가 파괴될 때 호출되는 함수로, OpenGL 리소스를 정리하고 콘텍스트를 해제한다.
  2. COpenGLView::OnSize 함수
    윈도우 크기 조정 이벤트를 처리하고 OpenGL 뷰포트와 투영 행렬을 설정한다.

그래픽 렌더링

  1. COpenGLView::RenderScene 함수
    이 함수는 실제 그래픽을 렌더링 하는 함수입니다. 여기에서는 행성과 달의 3D 모델링 및 회전 등을 구현한다.

OpenGL 초기화 설정

  1. COpenGLView::SetupRC 함수
    OpenGL 초기화 및 렌더링 설정을 수행하는 함수이며, OpenGL의 초기 설정, 렌더링 콘텍스트 설정, 그리기 옵션 등을 설정할 때 사용한다.
  2. COpenGLView::OnTimer 함수
    타이머 이벤트를 처리하는 함수로, 행성과 달의 회전을 업데이트하고 화면을 다시 그립니다.
  3. COpenGLView::OnEraseBkgnd 함수
    배경을 지우는 함수로, 이 예제에서는 배경을 지우지 않고 0을 반환하여 배경을 지우지 않습니다.
    밑에 따로 추가적인 설명이 있습니다!



    이렇게 글을 적으면서도  확실하게 머리속에 들어오지 않는다는 사실....

코드 구현

OnCreate() 함수 코드

int COpenGLView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    // CView 클래스의 OnCreate 함수를 호출
    if (CView::OnCreate(lpCreateStruct) == -1)
        return -1;

    int  nPixelFormat; // 픽셀 포맷 번호를 저장 변수
    m_hDC = ::GetDC(m_hWnd); // 현재 윈도우의 디바이스 컨텍스트를 가져와 m_hDC 변수에 저장

    static PIXELFORMATDESCRIPTOR pfd = {
        sizeof(PIXELFORMATDESCRIPTOR), // 픽셀 포맷 디스크립터의 크기 설정
        1, // 버전 번호 설정
        PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, // 윈도우에 그리기 및 OpenGL 지원 설정
        PFD_TYPE_RGBA, // RGBA 색상 모드 사용
        24, // 색상 비트 수 설정
        0, 0, 0, 0, 0, 0, // 사용하지 않는 비트 초기화
        0, 0, // 사용하지 않는 값 초기화
        0, 0, 0, 0, 0, // 사용하지 않는 값 초기화
        32, // 깊이 버퍼 비트 수 설정
        0, // 스텐실 버퍼를 사용하지 않도록 설정
        0, // 오버레이를 사용하지 않도록 설정
        PFD_MAIN_PLANE, // 주 플레인 설정
        0, // 레이어를 지원하지 않도록 설정
        0, 0, 0 // 사용하지 않는 값 초기화
    };

    nPixelFormat = ChoosePixelFormat(m_hDC, &pfd); // 적절한 픽셀 포맷 선택
    VERIFY(SetPixelFormat(m_hDC, nPixelFormat, &pfd)); // 선택한 픽셀 포맷 설정
    m_hRC = wglCreateContext(m_hDC); // OpenGL 컨텍스트 생성하고 m_hRC 변수에 저장
    VERIFY(wglMakeCurrent(m_hDC, m_hRC)); // 현재 스레드에서 OpenGL 컨텍스트 사용 설정

    SetupRC(); // OpenGL 렌더링 컨텍스트 설정 함수 호출

    SetTimer(1, 50, NULL); // 타이머 1 설정, 50ms마다 타이머 메시지 생성
    SetTimer(2, 17, NULL); // 타이머 2 설정, 17ms마다 타이머 메시지 생성

    return 0; // 함수 종료, 성공을 나타내는 값 반환
}


OnDestory() 함수 코드

void COpenGLView::OnDestroy()
{
    VERIFY(wglMakeCurrent(NULL, NULL)); // 현재 OpenGL 컨텍스트 해제
    wglDeleteContext(m_hRC); // OpenGL 컨텍스트 삭제
    ::ReleaseDC(m_hWnd, m_hDC); // 디바이스 컨텍스트 해제

    gluDeleteQuadric(m_objQuad); // Quadric 객체 삭제

    KillTimer(1); // 타이머 1 해제
    KillTimer(2); // 타이머 2 해제

    CView::OnDestroy(); // CView 클래스의 OnDestroy 함수 호출

    // TODO: 여기에 메시지 처리기 코드를 추가합니다.
}

 

OnSize() 함수 코드

void COpenGLView::OnSize(UINT nType, int cx, int cy)
{
    // 부모 클래스의 OnSize 함수를 호출한 후
    CView::OnSize(nType, cx, cy);

    // 화면 가로 세로 비율을 설정
    GLfloat fAspect;

    // 화면의 높이가 0인 경우를 방지
    if (cy == 0)
        cy = 1;

    // OpenGL의 viewport를 윈도우 크기에 맞게 설정
    glViewport(0, 0, cx, cy);

    // 가로 세로 비율을 계산
    fAspect = (GLfloat)cx / (GLfloat)cy;

    // 투영 행렬 모드로 전환하고 초기화
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    // 원근 투영 행렬을 설정
    gluPerspective(45.0f, fAspect, 1.0f, 400.0f);

    // 모델 뷰 행렬 모드로 전환하고 초기화
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}

 

OnDraw() 함수 코드

void COpenGLView::OnDraw(CDC* /*pDC*/)
{
    // 현재 문서를 가져온다.
    COpenGLDoc* pDoc = GetDocument();

    // 문서의 유효성을 검사하며
    ASSERT_VALID(pDoc);
    if (!pDoc)
        return; // 문서가 유효하지 않으면 함수를 종료
    
    // RenderScene 함수를 호출하여 3D 장면을 그린다
    RenderScene();

    // 그린 내용을 화면에 표시하기 위해 버퍼를 교체
    SwapBuffers(m_hDC);

    // TODO: 여기에 원시 데이터에 대한 그리기 코드를 추가합니다.
}

 

RenderScene() 함수 코드

void COpenGLView::RenderScene()
{
    // 색상 버퍼와 깊이 버퍼를 지우고, 화면을 초기화
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // 양면 그리기와 깊이 테스트 활성화
    glEnable(GL_CULL_FACE);
    glEnable(GL_DEPTH_TEST);
    gluQuadricNormals(m_objQuad, GLU_FLAT);

    // 모델뷰 행렬 모드로 전환

    glMatrixMode(GL_MODELVIEW);

    // 원점에서 일정 거리 떨어진 곳에 빨간 구를 그림
    glPushMatrix();
    glTranslatef(0.0, 0.0, -300.0); // (x축, y축, z축);로 위치 설정
    glColor3ub(255, 0, 0); // 빨간색으로 설정
    gluSphere(m_objQuad, 15, 32, 32); // 구를 그림
    glPopMatrix();

    // 파란 구를 지구처럼 보이게 하기 위해 회전하고 그림
    glPushMatrix();
    glTranslatef(105.0, 0.0, 0.0); // (x축, y축, z축);로 위치 설정
    glRotatef(m_fEarth, 0.0, 0.1, 0.0); // (각도, x축, y축, z축);로 회전 설정
    glColor3ub(0, 0, 255); // 파란색으로 설정
    gluSphere(m_objQuad, 15, 32, 32); // 구를 그림
    glPopMatrix();

    // 회색 구를 달처럼 보이게 하기 위해 회전하고 그림
    glPushMatrix();
    glTranslatef(30.0, 0.0, 0.0); // (x축, y축, z축);로 위치 설정
    glRotatef(m_fMoon, 0.0, 1.0, 0.0); // (각도, x축, y축, z축);로 회전 설정
    glColor3ub(200, 200, 200); // 회색으로 설정
    gluSphere(m_objQuad, 6, 32, 32); // 구를 그림
    glPopMatrix();
}

 

SetupRC() 함수 코드

void COpenGLView::SetupRC()
{
    // 배경색을 검은색(R=0, G=0, B=0)으로 설정하고 투명도(alpha)는 1.0으로 설정
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

    // OpenGL 배경색을 설정
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);

    // 그림자 모드를 설정
    glShadeModel(GL_FLAT);

    // 다각형의 정면을 설정
    glFrontFace(GL_CCW);

    // Quadric 객체를 생성하여 다양한 기하학적 도형을 그릴 수 있음
    m_objQuad = gluNewQuadric();

    // 타이머를 설정하여 200밀리초(0.2초)마다 Timer1 메시지를 생성
    SetTimer(1, 200, NULL);
}

 

Ontimer() 함수 코드

void COpenGLView::OnTimer(UINT_PTR nIDEvent)
{
    // 타이머 이벤트 처리
    // nIDEvent는 타이머의 ID를 나타냅니다.

    if (nIDEvent == 1) {
        // 타이머 ID가 1인 경우 지구(Earth)의 회전 각도를 6.0도 증가
        m_fEarth += 6.0f;

        // 회전 각도가 360도를 넘으면 0도로 초기화
        if (m_fEarth > 360.0f)
            m_fEarth = 6.0f;

        // 화면을 다시 그리도록 강제 업데이트
        InvalidateRect(NULL);
    }
    else if (nIDEvent == 2) {
        // 타이머 ID가 2인 경우 달(Moon)의 회전 각도를 6.0도 증가
        m_fMoon += 6.0f;

        // 회전 각도가 360도를 넘으면 360도를 빼서 0도로 만든다
        if (m_fMoon > 360.0f)
            m_fMoon -= 360.0f;

        // 화면을 다시 그리도록 강제 업데이트
        InvalidateRect(NULL);
    }

    // 부모 클래스의 OnTimer 함수를 호출
    CView::OnTimer(nIDEvent);
}

 

이렇게 각 함수들에 코드들을 입력한 후 실행을하면... 화면이 깜빡인다.. 신경이 쓰인다.

 

 

왜 이러한 현상이 발생하는지 구글링을 하여보니 Windows에서 WM_PAINT 메시지가 발생하는 상황인 것이다... 

  1. 윈도가 처음 생성될 때, 윈도의 위치가 변경될 때, 윈도의 크기가 변경될 때 (최소화 및 최대화 포함),
    윈도가 다른 윈도에 가려져 있던 부분이 나타날 때, 윈도가 스크롤될 때, UpdateWindow나 RedrawWindow 함수가 호출될 때, InvalidateRect나 InvalidateRgn 함수가 호출되어 다시 그려져야 할 영역이 발생하고, Message Queue에 처리해야 할 다른 Windows Message가 없을 때

WM_PAINT 메시지가 발생하기 '전'에 WM_ERASEBKGND 메시지가 보통 함께 전송됩니다.

WM_ERASEBKGND는 "배경을 지워라"를 의미하며, 기본적으로 Windows의 기본 메시지 프로시저(DefWindowProc)는 WM_ERASEBKGND를 받으면 WNDCLASS의 hbrBackground로 지정된 색상으로 배경을 지웁니다.

이로 인해 화면이 깜박이는 현상이 발생하며, 이를 방지하기 위해 WM_ERASEBKGND 메시지를 처리하여 배경을 지우지 않도록 할 수 있습니다. 일반적으로 WM_ERASEBKGND 메시지 핸들러인 OnEraseBkgnd에서 0을 반환하면 됩니다.

또한, 화면 깜빡임 문제가 자식 윈도 또는 컨트롤에 의해 발생하는 경우, 부모 윈도에 WS_CLIPCHILDREN 스타일을 적용하여 자식 컨트롤에 의해 가려지는 영역을 그리기에서 제외시킬 수 있습니다.

출처 : 데브피아 ( 박재민(MAXIST) )

 

따라서 클래스 마법사를 이용해 밑에 코드를 추가하면 현상이 해결이 된다.

BOOL COpenGLView::OnEraseBkgnd(CDC* pDC)
{
	// TODO: 여기에 메시지 처리기 코드를 추가 및/또는 기본값을 호출합니다.

	return 0; //CView::OnEraseBkgnd(pDC);
}

 

 

이렇게 태양과 지구 그리고 달까지 구현해 보았다.