OpenGL 3D computer graphics에서 사용되는 기초적인 프로그램입니다. 오픈소스인 만큼, 사용자나 환경에 대해 제약이 적다는 장점을 갖고 있습니다. OpenGL 요소 중에서도, GLSL shading language와 GLUT library를 활용하였습니다.
int main() {
initGlutState();
glewInit(); // load the OpenGL extensions
initGLState();
initShaders()
initGeometry();
glutMainLoop();
return 0;
}
Main 함수에서는 Glut, glew, shaders, geomtery에 대한 초기화 이후 glutMainLoop에서 주요 렌더링이 실행되는 구조를 지니고 있습니다.
static int g_width= 512; static int g_height= 512;
void initGlutState()
{
glutInit();
glutInitDisplayMode(GLUT_RGBA|GLUT_DOUBLE|GLUT_DEPTH);
glutInitWindowSize(g_width, g_height);
glutCreateWindow("Hello World");
glutDisplayFunc(display);
glutReshapeFunc(reshape);
}
void reshape(int w, int h) {
g_width = w;
g_height = h;
glViewport(0, 0, w, h);
glutPostRedisplay();
}
Glut 상태를 초기화하는 함수에서는 window와 관련된 정보를 초기화합니다. reshape는 window의 shape을 변경하고, Viewport를 띄우는 역할을 수행합니다.
static void initGLState() {
glClearColor(128./255., 200./255., 255./255., 0.); // RGBA
glClearDepth(0.);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glCullFace(GL_BACK); // back-face culling
glEnable(GL_CULL_FACE); // back-face culling
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_GREATER); // closer
glReadBuffer(GL_BACK);
if (!g_Gl2Compatible)
glEnable(GL_FRAMEBUFFER_SRGB);
}
GL state 초기화에서는, image colors 초기화, z-buffer 초기화 및 back-face culling과 큰 z value가 close를 의미하도록 설정합니다.
static const char * const g_shaderFilesGl2[g_numShaders][2] = {
{"./shaders/asst1-gl2.vshader", "./shaders/asst1-gl2.fshader"}
};
static void initShaders() {
g_shaderStates.resize(g_numShaders);
for (int i = 0; i < g_numShaders; ++i) {
if (g_Gl2Compatible)
g_shaderStates[i].reset(new ShaderState(g_shaderFilesGl2[i][0], g_shaderFilesGl2[i][1]));
else
g_shaderStates[i].reset(new ShaderState(g_shaderFiles[i][0], g_shaderFiles[i][1]));
}
}
Vertex shader와 Fragment shader를 지정하는 부분입니다. 각 shader가 어떤 역할을 수행하는지 알아보겠습니다.

Types of variables:
uniform mat4 uProjMatrix;
uniform mat4 uModelViewMatrix;
uniform mat4 uNormalMatrix;
attribute vec3 aPosition;
attribute vec3 aNormal;
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
vNormal = vec3(uNormalMatrix * vec4(aNormal, 0.0));
// send position (eye coordinates) to fragment shader
vec4 tPosition = uModelViewMatrix * vec4(aPosition, 1.0);
vPosition = vec3(tPosition);
gl_Position = uProjMatrix * tPosition;
}
모든 vertex position의 Object 좌표를 MVM 행렬을 통해 eye 좌표로 바꾸는 역할을 합니다. Camera matrix(projection)을 적용하여 gl_Position을 반홥합니다. normal 벡터 또한 반환합니다.

triangle 내에 screen pixels를 찾은 후, varing variables에 대해 보간하는 Rasterization 과정을 거칩니다. 따라서 vPosition에 해당하는 각 픽셀은 geometric position of the point를 의미합니다. 계산한 좌표 값은 Fragment shader에서 appearance를 렌더링 하는 데에 사용됩니다.

Fragment shader는 uniform 변수(light, color)를 통해 material appearance를 계산합니다. BRDF(Bidirectional reflectance distribution function) 등의 렌더링 방법으로 appearance를 표현합니다.
uniform vec3 uLight, uLight2, uColor;
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
vec3 tolight = normalize(uLight - vPosition);
vec3 tolight2 = normalize(uLight2 - vPosition);
vec3 normal = normalize(vNormal);
float diffuse = max(0.0, dot(normal, tolight));
diffuse += max(0.0, dot(normal, tolight2));
vec3 intensity = uColor * diffuse;
gl_FragColor = vec4(intensity, 1.0);
}
Fragment shader의 기초 코드입니다. BRDF에서 reflectance function만 구현된 부분입니다. point light 쪽으로 방향을 계산하고, 각 light vector의 내적 합을 diffuse로 사용하여 appearance를 렌더링합니다.
static void initGeometry() {
initGround();
initCubes();
}
static void initGround() {
// A x-z plane at y = g_groundY of dimension [-g_groundSize, g_groundSize]^2
VertexPN vtx[4] = {
VertexPN(-g_groundSize, g_groundY, -g_groundSize, 0, 1, 0),
VertexPN(-g_groundSize, g_groundY, g_groundSize, 0, 1, 0),
VertexPN( g_groundSize, g_groundY, g_groundSize, 0, 1, 0),
VertexPN( g_groundSize, g_groundY, -g_groundSize, 0, 1, 0),
};
unsigned short idx[] = {0, 1, 2, 0, 2, 3};
g_ground.reset(new Geometry(&vtx[0], &idx[0], 4, 6));
}
static void initCubes() {
int ibLen, vbLen;
getCubeVbIbLen(vbLen, ibLen);
// Temporary storage for cube geometry
vector<VertexPN> vtx(vbLen);
vector<unsigned short> idx(ibLen);
makeCube(1, vtx.begin(), idx.begin());
g_cube.reset(new Geometry(&vtx[0], &idx[0], vbLen, ibLen));
}
Ground와 Cube 등 Geometry를 초기화하는 부분입니다. makeCube 함수에서는 position, normal, texture 정보를 담는 vertex structure를 생성합니다.
struct Geometry {
GlBufferObject vbo, ibo;
int vboLen, iboLen;
Geometry(VertexPN *vtx, unsigned short *idx, int vboLen, int iboLen) {
this->vboLen = vboLen;
this->iboLen = iboLen;
// Now create the VBO and IBO
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(VertexPN) * vboLen, vtx, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned short) * iboLen, idx, GL_STATIC_DRAW);
}
void draw(const ShaderState& curSS) {
// Enable the attributes used by our shader
safe_glEnableVertexAttribArray(curSS.h_aPosition);
safe_glEnableVertexAttribArray(curSS.h_aNormal);
// bind vbo
glBindBuffer(GL_ARRAY_BUFFER, vbo);
safe_glVertexAttribPointer(curSS.h_aPosition, 3, GL_FLOAT, GL_FALSE, sizeof(VertexPN), FIELD_OFFSET(VertexPN, p));
safe_glVertexAttribPointer(curSS.h_aNormal, 3, GL_FLOAT, GL_FALSE, sizeof(VertexPN), FIELD_OFFSET(VertexPN, n));
// bind ibo
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
// draw!
glDrawElements(GL_TRIANGLES, iboLen, GL_UNSIGNED_SHORT, 0);
// Disable the attributes used by our shader
safe_glDisableVertexAttribArray(curSS.h_aPosition);
safe_glDisableVertexAttribArray(curSS.h_aNormal);
}
};
Geometry에서는 Vertex Buffer Object(VBO)와 Index Buffer Object(IBO)를 생성하고, bind 후 GL buffer에 data를 전송합니다. buffer에 저장된 데이터를 draw에서도 사용합니다.
static void drawStuff() {
// short hand for current shader state
const ShaderState& curSS = *g_shaderStates[g_activeShader];
// build & send proj. matrix to vshader
const Matrix4 projmat = makeProjectionMatrix();
sendProjectionMatrix(curSS, projmat);
// eyeRbt
const Matrix4 eyeRbt = g_eyeRbt;
const Matrix4 invEyeRbt = inv(eyeRbt);
const Cvec3 eyeLight1 = Cvec3(invEyeRbt * Cvec4(g_light1, 1)); // g_light1 position in eye coordinates
const Cvec3 eyeLight2 = Cvec3(invEyeRbt * Cvec4(g_light2, 1)); // g_light2 position in eye coordinates
safe_glUniform3f(curSS.h_uLight, eyeLight1[0], eyeLight1[1], eyeLight1[2]);
safe_glUniform3f(curSS.h_uLight2, eyeLight2[0], eyeLight2[1], eyeLight2[2]);
// draw ground
// ===========
//
const Matrix4 groundRbt = Matrix4(); // identity
Matrix4 MVM = invEyeRbt * groundRbt; // model view matrix for each object
Matrix4 NMVM = normalMatrix(MVM);
sendModelViewNormalMatrix(curSS, MVM, NMVM);
safe_glUniform3f(curSS.h_uColor, 0.1, 0.95, 0.1); // set color
g_ground->draw(curSS);
// draw cubes
// ==========
for (int i = 0; i < numCubes; ++i) {
MVM = invEyeRbt * g_objectRbt[i];
NMVM = normalMatrix(MVM);
sendModelViewNormalMatrix(curSS, MVM, NMVM);
safe_glUniform3f(curSS.h_uColor, g_objectColors[i][0], g_objectColors[i][1], g_objectColors[i][2]);
g_cube->draw(curSS);
}
}
두 개의 큐브를 그리는 OpenGL program을 구현하는 과제입니다. 1️⃣ 'v'를 입력했을 때, sky-camera frame -> frame of cube 1 -> frame of cube 2로 view가 변경되어야 하며 2️⃣ 'o' 입력에 따라 object에 대해 manipulation을 적용해야 합니다. 3️⃣ eye와 manipulation 대상이 모두 sky camera일 때는 'm'을 입력하여 World-sky frame과 Sky-sky frame을 변경할 수 있어야 합니다.
???-??? frame은 Manipulation 대상과 eye 관계에 따른 frame 명칭입니다. 예시로, 현재 cube가 조정되고 eye가 sky camera인 상태라면, cube-sky frame이 됩니다. 아래에서는 HelloWorld3D 프로그램을 구현하기 위해 필요한 개념들을 소개하겠습니다. vector, frame, affine transformation 등의 자세한 기초 개념은 생략하고, frame 간의 이동 및 고정된 eye에서 물체 이동 등의 개념을 위주로 다루겠습니다.

frame에 대해 Transformation을 적용할 때, Left-of rule을 적용합니다. 예를 들어, 와 같은 scale transformation이 있다면, frame 는 frame에 대해 S 변환이 일어난 것입니다.

변환 전의 point 를 중심으로, frame 에 대한 rotation은 로 표현되며, auxiliary frame 에 대한 rotation은 로 표현되게 됩니다.
object frame에 Moving transform 을 적용하는 과정입니다. 위에서 설명한 auxiliary frame에 대해 Rotation/Translation을 모두 적용하는 Affine transformation matrix 으로 변경한 것입니다. object frame을 auxiliary frame으로 표현 후, 변환을 거쳐 world frame으로 표현할 수 있게 됩니다.
이때, auxiliary frame 을 표현하기 위해서는 에 필요한 Affine transform 를 정의해야 합니다.

frame을 넘어 object를 조작하려면, object의 origin에 대한 translation 정보와, eye의 y축에 대한 object의 rotation 정보가 필요하기 때문에, 로 표현할 수 있습니다. 따라서, world frame을 object frame의 origin 로 변환하고, M으로 object frame을 eye 의 방향에 놓여 회전시켜 object를 움직입니다.
eye를 옮길 시에는 위와 같은 auxiliary 좌표계에서 eye frame에 affine transform을 직접적으로 적용하여 구현합니다. 단, moving transform을 적용 시에는 inverse를 적용합니다. ( frame이 view matrix에서 이미 inverse되기 때문입니다.)
해당 프로그램을 구현한 저의 코드는 깃허브에서 확인하실 수 있습니다.

[1] Foundations of 3D Computer graphics, http://www.3dgraphicsfoundations.com/
[2] OpenGL, https://www.khronos.org/opengl/
[3] GLEW, https://glew.sourceforge.net/
[4] GLUT, https://www.transmissionzero.co.uk/software/freeglut-devel/
[5] https://mhsung.github.io/kaist-cs380-spring-2023/