8 분 소요

1. 개요

OpenGL의 GLUT 라이브러리를 활용하여 3차원 그래픽스에서의 조명 효과를 실험하는 목적으로 이 문서를 작성했다. 두 주전자와 조명을 공간에 배치하여 다양한 조명 조건에서의 물체 렌더링 효과를 표현하였고, 또한 카메라 애니메이션을 통해 조명과 재질의 상호작용을 여러 각도에서 관찰할 수 있도록 구현하였다.

사실 대학교 기말과제이다.

2. 배경

2.1 조명

컴퓨터 그래픽스에서 조명은 3차원 객체의 사실적인 렌더링을 위한 핵심 요소이다. 실제 세계의 빛의 특성을 모델링하여 가상 객체에 깊이감, 질감, 입체감을 부여하며, 조명 모델은 크게 세 가지로 나뉜다:

종류 설명
Ambient Light (환경광) 간접적으로 반사된 빛으로, 그림자 영역에서도 객체가 완전히 어둡지 않게 하는 기본 조명.
Diffuse Light (확산광) 표면에서 모든 방향으로 균등하게 반사되는 빛으로, 객체의 기본 색상을 결정.
Specular Light (정반사광) 특정 방향으로 강하게 반사되는 빛으로, 표면의 광택하이라이트를 표현

2.2 재질

3차원 객체의 재질 특성은 광원과의 상호작용을 결정한다. 재질의 반사도(shininess), 확산 반사율, 정반사율 등의 속성에 따라 같은 조명 조건에서도 완전히 다른 시각적 효과를 얻을 수 있다. 금속, 플라스틱, 천 등 다양한 재질을 표현하는데 활용된다.

2.3 다중 조명을 통한 비교

실제 환경에서는 여러 개의 조명원이 동시에 존재하며, 각각의 조명이 객체에 미치는 영향이 합성되어 최종 렌더링 결과가 결정된다.

3. 제작 방법

3.1 개발 환경 및 라이브러리

  • 개발 언어: C++
  • 그래픽스 라이브러리: OpenGL 3.x
  • 윈도우 관리: GLUT (OpenGL Utility Toolkit)
    • deprecated된지 20년 정도 된 오래된 라이브러리지만 어쩌겠는가, 교수님께서 이걸로 수업하시는걸.

3.2 시스템 구조

3.2.1 핵심 구성 요소

카메라 시스템

 void CalculateCameraPos()
 {
     if (bShouldStop)
         return;
 
     Angle += Speed * DeltaTime;
     Angle = fmod(Angle, 360.0f);
 
     X = cos(Angle * Deg2Rad) * Radius;
     Y = sin(Angle * Deg2Rad) * Radius;
     Z = sin(Angle * Deg2Rad) * Radius;
 }

원형 궤도를 따라 자동으로 회전하는 카메라를 구현하여 객체를 다양한 각도에서 관찰할 수 있도록 하였다. 중점을 기준으로 주변을 물결치며(sin, cos) 공전하도록 구현하였다.

조명 시스템

  • LIGHT0: 포인트 라이트 (Point Light) - 원점에 위치, 따뜻한 황색 조명 같은 광원 구성에 유리
  • LIGHT1: 방향성 라이트 (Directional Light) - 방향성 조명, 태양빛 같은 전 지역 조명 구성에 유리
    • 라이트가 자연스럽게 섞이고 상호작용 되는것을 쉽게 보기 위해서 LIGHT1도 포인트 라이트로 만들었다.
void LightSpec()
{
    GLfloat global_ambient[] = { 0.1, 0.1, 0.1, 1.0 };	// 전역 환경광
    GLfloat light0_ambient[] = { 0.5, 0.4, 0.3, 1.0 };	// 광원0 환경광
    GLfloat light0_diffuse[] = { 0.5, 0.4, 0.3, 1.0 };	// 광원0 확산광
    GLfloat light0_specular[] = { 1.0, 1.0, 0, 1.0 };	// 광원0 정반사광

    GLfloat light1_ambient[] = { 0.0, 0.0, 0.0, 1.0 };	// 광원1 환경광
    GLfloat light1_diffuse[] = { 0.5, 0.2, 0.3, 1.0 };	// 광원1 확산광
    GLfloat light1_specular[] = { 0.0, 0.0, 1.0, 1.0 };	// 광원1 정반사광

    glShadeModel(GL_SMOOTH);
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_LIGHTING);

    // 조명 ON/OFF 제어
    if (bLight0Enabled) {
        glEnable(GL_LIGHT0);
        glLightfv(GL_LIGHT0, GL_AMBIENT, light0_ambient);
        glLightfv(GL_LIGHT0, GL_DIFFUSE, light0_diffuse);
        glLightfv(GL_LIGHT0, GL_SPECULAR, light0_specular);
    }
    else {
        glDisable(GL_LIGHT0);
    }

    if (bLight1Enabled) {
        glEnable(GL_LIGHT1);
        glLightfv(GL_LIGHT1, GL_AMBIENT, light1_ambient);
        glLightfv(GL_LIGHT1, GL_DIFFUSE, light1_diffuse);
        glLightfv(GL_LIGHT1, GL_SPECULAR, light1_specular);
    }
    else {
        glDisable(GL_LIGHT1);
    }

    glLightModelfv(GL_LIGHT_MODEL_AMBIENT, global_ambient);
    glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);
}

3.2.2 재질 시스템

세 가지 서로 다른 재질 모드를 구현하여 조명과 재질의 상호작용을 비교 분석할 수 있도록 하였다:

  1. 보통 재질: 중간 정도의 반사도 (shininess: 25)
  2. 유광 재질: 높은 반사도 (shininess: 128)로 메탈릭한 효과
  3. 무광 재질: 낮은 반사도 (shininess: 1)로 매트한 효과

3.3 간단한 제어 기능

사용자가 실시간으로 조명 조건을 변경하며 실험할 수 있도록 다음과 같은 키보드 인터페이스를 구현하였다:

기능 설명
a 애니메이션 제어 카메라 회전 일시정지/재생
1 LIGHT0 제어 첫 번째 조명 ON/OFF
2 LIGHT1 제어 두 번째 조명 ON/OFF
m 재질 변경 세 가지 재질 모드 순환
= 속도 증가 카메라 회전 속도 증가
- 속도 감소 카메라 회전 속도 감소
ESC 프로그램 종료 애플리케이션 종료

3.4 렌더링 파이프라인

  1. 초기화 단계: 조명 속성 설정, 재질 속성 정의
  2. 애니메이션 단계: 델타 타임 기반 카메라 위치 계산
  3. 조명 설정 단계: 활성화된 조명원의 위치 및 속성 적용
  4. 렌더링 단계: 현재 재질 모드에 따른 주전자 객체 렌더링
  5. 시각화 단계: 조명 위치를 나타내는 구체 렌더링

3.5 사용 된 기법

  • 실시간 조명 계산: OpenGL의 하드웨어 가속 조명 계산 활용
  • 부드러운 쉐이딩: GL_SMOOTH 쉐이딩 모드로 자연스러운 표면 표현
  • 깊이 버퍼링: 정확한 3차원 객체 렌더링을 위한 Z-버퍼 활용
  • 타이밍 기반 애니메이션: Update함수 구현을 통한 프레임 독립적인 카메라 움직임

4. 결과 및 분석

4.1 실행

해파리이(가) 표시된 사진 AI 생성 콘텐츠는 정확하지 않을 수 있습니다.

첫 실행 모습. 화면에 보이는 조명 구체는 표시용으로, 전구는 빛의 영향을 받지 아니함.

4.2 노란 전구만 활성화

양초, 정물 사진, 조명, 빛이(가) 표시된 사진 AI 생성 콘텐츠는 정확하지 않을 수 있습니다.

파란 전구가 비활성화 되면서, 노란 빛만이 반사되는 모습을 확인 가능.

4.3 파란 전구만 활성화

img

파란 전구만 활성화 한 모습, 노란 전구와 달리 정 중앙이 아니라 좀 더 바깥쪽에 배치하여 반사가 되는 모습을 쉽게 관찰 가능.

4.4 유광 재질로 변경

스크린샷, 램프, 빛이(가) 표시된 사진 AI 생성 콘텐츠는 정확하지 않을 수 있습니다.

노란 전구만 활성화하고 유광 재질로 변경했다. 노란 빛의 광원이 선명히 반사되고 있다.

4.5 무광 재질로 변경

정물 사진, 어둠, 빛이(가) 표시된 사진 AI 생성 콘텐츠는 정확하지 않을 수 있습니다.

파란 전구만 활성화하고 무광 재질로 변경한 모습, 파란 빛의 광원이 매트하게 반사되고 있다.

4.6 유광 재질과 두 전구의 상호작용

구체, 해파리, 달이(가) 표시된 사진 AI 생성 콘텐츠는 정확하지 않을 수 있습니다.

사진을 보면, 유광 재질의 주전자에 두 개의 전구가 비춰 보이는 것을 확인할 수 있다. 이는 정상적으로 전구의 반사작용이 동작하고 있음을 보여준다.

5. 결론

본 프로젝트를 통해 OpenGL을 활용한 3차원 조명 시스템의 구현과 조명과 재질의 상호작용을 이해할 수 있게 되었다.

5.1 주요 성과

기술적 성과

  • 다중 포인트 라이트를 활용한 복합 조명 구현
  • 세 가지의 다양한 재질 표현
  • 카메라 애니메이션을 통한 다각도 관찰

학습적 성과

  • Ambient, Diffuse, Specular 조명 성분의 실질적 이해
  • 재질의 반사도(shininess)가 시각적 표현에 미치는 영향 확인
  • OpenGL 렌더링 파이프라인에 대한 기능적 이해

개발 과정에서의 문제 해결

파란 조명의 위치와, 실질적으로 파란 빛이 비치는 방향이 알맞지 않는 오류가 있었다. 이는 조명 위치 설정이 뷰 변환(gluLookAt)보다 먼저 실행되어 월드/뷰 좌표계 간의 변환이 꼬인 것이 원인이었다. 이를 해결하기 위해 조명 위치 설정을 뷰 변환 이후로 이동하여 정확한 조명 효과를 구현할 수 있었다.

실행 영상

6. 전체 소스코드

#define _USE_MATH_DEFINES

#include <gl/glut.h>
#include <gl/glu.h>
#include <math.h>
#include <iostream>

//#define Deg2Rad (M_PI / 180)

int LastTime = 0;
float DeltaTime = 0;

bool bShouldStop = false;
float Speed = 100;
float Angle = 0;
float X = 0, Y = 0, Z = 0;
float Radius = 1.0;

bool bLight0Enabled = true;
bool bLight1Enabled = true;
int materialMode = 0;  // 0: 기본, 1: 반짝임, 2: 무광

GLfloat LightPosition0[] = { 0, 0, 0, 1 };
GLfloat LightPosition1[] = { 1.5, 1.5, 1.5, 1 };

void CalculateCameraPos()
{
    if (bShouldStop)
        return;

    Angle += Speed * DeltaTime;
    Angle = fmod(Angle, 360.0f);

    X = cos(Angle * Deg2Rad) * Radius;
    Y = sin(Angle * Deg2Rad) * Radius;
    Z = sin(Angle * Deg2Rad) * Radius;
}

void LightSpec()
{
    GLfloat global_ambient[] = { 0.1, 0.1, 0.1, 1.0 };
    GLfloat light0_ambient[] = { 0.5, 0.4, 0.3, 1.0 };
    GLfloat light0_diffuse[] = { 0.5, 0.4, 0.3, 1.0 };
    GLfloat light0_specular[] = { 1.0, 1.0, 0, 1.0 };

    GLfloat light1_ambient[] = { 0.0, 0.0, 0.0, 1.0 };
    GLfloat light1_diffuse[] = { 0.5, 0.2, 0.3, 1.0 };
    GLfloat light1_specular[] = { 0.0, 0.0, 1.0, 1.0 };

    glShadeModel(GL_SMOOTH);
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_LIGHTING);

    // 조명 ON/OFF 제어
    if (bLight0Enabled) {
        glEnable(GL_LIGHT0);
        glLightfv(GL_LIGHT0, GL_AMBIENT, light0_ambient);
        glLightfv(GL_LIGHT0, GL_DIFFUSE, light0_diffuse);
        glLightfv(GL_LIGHT0, GL_SPECULAR, light0_specular);
    }
    else {
        glDisable(GL_LIGHT0);
    }

    if (bLight1Enabled) {
        glEnable(GL_LIGHT1);
        glLightfv(GL_LIGHT1, GL_AMBIENT, light1_ambient);
        glLightfv(GL_LIGHT1, GL_DIFFUSE, light1_diffuse);
        glLightfv(GL_LIGHT1, GL_SPECULAR, light1_specular);
    }
    else {
        glDisable(GL_LIGHT1);
    }

    glLightModelfv(GL_LIGHT_MODEL_AMBIENT, global_ambient);
    glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);
}

// 기본 재질
void AddDefaultTeapot(GLfloat x, GLfloat y, GLfloat z, GLfloat size)
{
    GLfloat teapot_ambient[] = { 0.3, 0.3, 0.3, 1.0 };
    GLfloat teapot_diffuse[] = { 0.8, 0.8, 0.8, 1.0 };
    GLfloat teapot_specular[] = { 0.5, 0.5, 0.5, 1.0 };
    GLfloat teapot_shininess[] = { 25.0 };

    glPushMatrix();
    glMaterialfv(GL_FRONT, GL_AMBIENT, teapot_ambient);
    glMaterialfv(GL_FRONT, GL_DIFFUSE, teapot_diffuse);
    glMaterialfv(GL_FRONT, GL_SPECULAR, teapot_specular);
    glMaterialfv(GL_FRONT, GL_SHININESS, teapot_shininess);

    glTranslatef(x, y, z);
    glutSolidTeapot(size);
    glPopMatrix();
}

// 유광 재질
void AddShinyTeapot(GLfloat x, GLfloat y, GLfloat z, GLfloat size)
{
    GLfloat teapot_ambient[] = { 0.2, 0.2, 0.3, 1.0 };
    GLfloat teapot_diffuse[] = { 0.6, 0.6, 0.8, 1.0 };
    GLfloat teapot_specular[] = { 1.0, 1.0, 1.0, 1.0 };  // 높은 스펙큘러
    GLfloat teapot_shininess[] = { 128.0 };  // 매우 높은 반사도

    glPushMatrix();
    glMaterialfv(GL_FRONT, GL_AMBIENT, teapot_ambient);
    glMaterialfv(GL_FRONT, GL_DIFFUSE, teapot_diffuse);
    glMaterialfv(GL_FRONT, GL_SPECULAR, teapot_specular);
    glMaterialfv(GL_FRONT, GL_SHININESS, teapot_shininess);

    glTranslatef(x, y, z);
    glutSolidTeapot(size);
    glPopMatrix();
}

// 무광 재질
void AddMatteTeapot(GLfloat x, GLfloat y, GLfloat z, GLfloat size)
{
    GLfloat teapot_ambient[] = { 0.4, 0.2, 0.2, 1.0 };
    GLfloat teapot_diffuse[] = { 0.8, 0.4, 0.4, 1.0 };
    GLfloat teapot_specular[] = { 0.1, 0.1, 0.1, 1.0 };  // 낮은 스펙큘러
    GLfloat teapot_shininess[] = { 1.0 };  // 매우 낮은 반사도

    glPushMatrix();
    glMaterialfv(GL_FRONT, GL_AMBIENT, teapot_ambient);
    glMaterialfv(GL_FRONT, GL_DIFFUSE, teapot_diffuse);
    glMaterialfv(GL_FRONT, GL_SPECULAR, teapot_specular);
    glMaterialfv(GL_FRONT, GL_SHININESS, teapot_shininess);

    glTranslatef(x, y, z);
    glutSolidTeapot(size);
    glPopMatrix();
}

void RenderTeapots()
{
    switch (materialMode) {
    case 0:
        AddDefaultTeapot(1, 1, -0.2, 0.5);
        AddDefaultTeapot(-1, -0.5, 0.3, 0.5);
        break;
    case 1:
        AddShinyTeapot(1, 1, -0.2, 0.5);
        AddShinyTeapot(-1, -0.5, 0.3, 0.5);
        break;
    case 2:
        AddMatteTeapot(1, 1, -0.2, 0.5);
        AddMatteTeapot(-1, -0.5, 0.3, 0.5);
        break;
    }
}

void RenderBulbs() 
{
    // 조명 위치 시각화 (구체)
    GLfloat bulb_ambient[] = { 0, 0, 0, 1 };
    GLfloat bulb_diffuse[] = { 0, 0, 0, 1 };
    GLfloat bulb_specular[] = { 1, 1, 1, 0 };
    GLfloat bulb_shininess[] = { 0 };

    glPushMatrix();
    glMaterialfv(GL_FRONT, GL_AMBIENT, bulb_ambient);
    glMaterialfv(GL_FRONT, GL_DIFFUSE, bulb_diffuse);
    glMaterialfv(GL_FRONT, GL_SPECULAR, bulb_specular);
    glMaterialfv(GL_FRONT, GL_SHININESS, bulb_shininess);

    glDisable(GL_LIGHTING);

    if (bLight0Enabled) {
        glColor3f(1.0f, 1.0f, 0.0f);
        glPushMatrix();
        glTranslatef(LightPosition0[0], LightPosition0[1], LightPosition0[2]);
        glutSolidSphere(0.1f, 10, 10);
        glPopMatrix();
    }
    if (bLight1Enabled) {
        glColor3f(0, 0, 1);
        glPushMatrix();
        glTranslatef(LightPosition1[0], LightPosition1[1], LightPosition1[2]);
        glutSolidSphere(0.1f, 10, 10);
        glPopMatrix();
    }

    glEnable(GL_LIGHTING);
    glPopMatrix();
}

void Display()
{
    LightSpec();
    CalculateCameraPos();

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    gluLookAt(X, Y, Z, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

    glLightfv(GL_LIGHT0, GL_POSITION, LightPosition0);
    glLightfv(GL_LIGHT1, GL_POSITION, LightPosition1);

    RenderTeapots();
    RenderBulbs();

    glFlush();
}

void Reshape(int width, int height)
{
    glViewport(0, 0, (GLsizei)width, (GLsizei)height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-4.0, 4.0, -4.0, 4.0, -4.0, 4.0);
    glMatrixMode(GL_MODELVIEW);
}

void KeyBoard(unsigned char key, int x, int y)
{
    switch (key)
    {
    case 'a':
        bShouldStop = !bShouldStop;
        break;
    case '1':  // 0번 라이트 끄기
        bLight0Enabled = !bLight0Enabled;
        break;
    case '2':  // 1번 라이트 끄기
        bLight1Enabled = !bLight1Enabled;
        break;
    case 'm':  // 재질 모드 변경
        materialMode = (materialMode + 1) % 3;
        break;
    case '=':  // 카메라 빠르게
        Speed += 20;
        if (Speed > 300) Speed = 300;
        break;
    case '-':  // 카메라 느리게
        Speed -= 20;
        if (Speed < 20) Speed = 20;
        break;
    case 27:   // ESC 키 (ASCII 코드 27)
        std::cout << "프로그램을 종료합니다." << std::endl;
        exit(0);
        break;
    default:
        break;
    }

    std::cout << key << " 입력 됨." << std::endl;
    glutPostRedisplay();
}

void Init()
{
    LastTime = glutGet(GLUT_ELAPSED_TIME);
}

void Update()
{
    int currentTime = glutGet(GLUT_ELAPSED_TIME);
    DeltaTime = (currentTime - LastTime) / 1000.0f;
    LastTime = currentTime;

    glutPostRedisplay();
}

int main(int argc, char** argv)
{
    glutInit(&argc, argv);
    glutInitWindowSize(600, 600);
    glutInitWindowPosition(100, 100);
    glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA | GLUT_DEPTH);

    Init();

    glutCreateWindow("라이팅 테스트");
    glClearColor(0, 0, 0, 0);
    glutDisplayFunc(Display);
    glutIdleFunc(Update);
    glutReshapeFunc(Reshape);
    glutKeyboardFunc(KeyBoard);

    std::cout << "=== 라이팅 테스트 ===" << std::endl;
    std::cout << "키 조작법:" << std::endl;
    std::cout << "a: 카메라 애니메이션 일시정지/재생" << std::endl;
    std::cout << "1: LIGHT0 (노란색) ON/OFF" << std::endl;
    std::cout << "2: LIGHT1 (파란색) ON/OFF" << std::endl;
    std::cout << "m: 재질 모드 변경 (기본 -> 반짝임 -> 무광)" << std::endl;
    std::cout << "=: 카메라 속도 증가" << std::endl;
    std::cout << "-: 카메라 속도 감소" << std::endl;
    std::cout << "ESC: 프로그램 종료" << std::endl;
    std::cout << "=======================" << std::endl;

    glutMainLoop();
}

업데이트: