OpenGL

김민솔·2024년 12월 24일

Graphics

목록 보기
1/4

OpenGL 3D computer graphics에서 사용되는 기초적인 프로그램입니다. 오픈소스인 만큼, 사용자나 환경에 대해 제약이 적다는 장점을 갖고 있습니다. OpenGL 요소 중에서도, GLSL shading language와 GLUT library를 활용하였습니다.

  • OpenGL with GLSL, GLUT 모두 cross-platform(Windows, Linux, MacOS...)이라는 장점이 있습니다.
  • GLSL은 각 vertex, fragment에 필요한 프로그램 구현이 용이합니다.
  • GLUT는 callback function을 정의하여 사용합니다. 마우스, 키보드 등의 event에 따라 callback function이 실행되도록 사용하기도 합니다.
    이번 포스트에서는 OpenGL에 대한 간략적인 개요와, OpenGL을 활용하여 HelloWorld3D 프로그램을 구현하는 내용을 담았습니다.

Main Program

int main() {
  initGlutState();
  glewInit(); // load the OpenGL extensions
  initGLState();
  initShaders()
  initGeometry();
  glutMainLoop();
  return 0;
}

Main 함수에서는 Glut, glew, shaders, geomtery에 대한 초기화 이후 glutMainLoop에서 주요 렌더링이 실행되는 구조를 지니고 있습니다.

initGlutState

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를 띄우는 역할을 수행합니다.

initGLState

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를 의미하도록 설정합니다.


Shader

initShaders

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가 어떤 역할을 수행하는지 알아보겠습니다.

Vertex shader

Types of variables:

  • uniform: does not change per primitives; read-only in shaders
  • in (vertex sh.): input changes per vertex, read-only;
  • in (frag. sh.): interpolated input; read-only
  • out: shader-output; VS to FS; FS output.
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

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를 렌더링합니다.


Geometry

initGeometry

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를 생성합니다.

Geometry(code)

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에서도 사용합니다.


Display

Modelview matrix

p~=otc=wtOc=etE1Oc\tilde{p} = \mathbf{o}^t\mathbf{c}=\mathbf{w}^tO\mathbf{c}=\mathbf{e}^tE^{-1}O\mathbf{c}
  • 이때 t는 전치가 아닌, frame을 의미
  • ot=wtO\mathbf{o}^{t}=\mathbf{w}^{t}O: object frame은 world frame에 object matrix OO를 적용하여 표현
  • et=wtEe^{t}=\mathbf{w}^{t}E: eye frame은 world frame에 eye matrix EE를 적용하여 표현
    Modelview matrix(MVM)은 view matrix인 E1E^{-1}와 object OO의 방향 및 위치(wrt the eye frame et{\mathbf{e}^t}를 나타냅니다. 위의 수식은 point p~\tilde{p}를 eye frame에 나타내는 과정입니다. vertex shader가 해당 vertex data들로부터 E1OcE^{-1}O\mathbf{c} 곱 연산을 수행하여, eye 좌표계를 나타냅니다.
    normalMatrix()로 linear factor의 inverse transpose를 적용하여 uniform normal NMVM을 얻습니다.

Code

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);
  }
}

HelloWorld3D

두 개의 큐브를 그리는 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

  • red: the world frame
  • green: the object frame
  • blue: the eye frame
    우리가 다루게 되는 frame 상태입니다. world frame -> object frame -> eye frame의 변환 단계를 거쳐 window를 통해 cube를 바라보게 됩니다.

Left-of rule

frame에 대해 Transformation을 적용할 때, Left-of rule을 적용합니다. 예를 들어, ft=atA1atSA1\mathbf{f}^{t} = \mathbf{a}^{t}A^{-1}\rightarrow\mathbf{a}^{t}SA^{-1}와 같은 scale transformation이 있다면, frame f\mathbf{f}a\mathbf{a} frame에 대해 S 변환이 일어난 것입니다.

Transfom a point over frame


변환 전의 point p~\tilde{p}를 중심으로, frame f\mathbf{f}에 대한 rotation은 ftRc=atA1Rc\mathbf{f}^{t}Rc=\mathbf{a}^{t}A^{-1}Rc로 표현되며, auxiliary frame at\mathbf{a}^t에 대한 rotation은 ftARA1c=atRA1c\mathbf{f}^{t}ARA^{-1}c=\mathbf{a}^{t}RA^{-1}c로 표현되게 됩니다.

Moving the object

ot=wtO=atA1O=>atMA1O=wtAMA1O\begin{aligned} \mathbf{o}^{t}&=\mathbf{w}^{t}O=\mathbf{a}^{t}A^{-1}O\\ &=> \mathbf{a}^{t}MA^{-1}O = \mathbf{w}^tAMA^{-1}O \end{aligned}

object frame에 Moving transform MM을 적용하는 과정입니다. 위에서 설명한 auxiliary frame에 대해 Rotation/Translation을 모두 적용하는 Affine transformation matrix MM으로 변경한 것입니다. object frame을 auxiliary frame으로 표현 후, 변환을 거쳐 world frame으로 표현할 수 있게 됩니다.

이때, auxiliary frame at\mathbf{a}^t을 표현하기 위해서는 wtA\mathbf{w}^tA에 필요한 Affine transform A=TRA=TR를 정의해야 합니다.

frame을 넘어 object를 조작하려면, object의 origin에 대한 translation 정보와, eye의 y축에 대한 object의 rotation 정보가 필요하기 때문에, A=(O)T(E)RA = (O)_{T}(E)_{R}로 표현할 수 있습니다. 따라서, world frame을 object frame의 origin OO로 변환하고, M으로 object frame을 eye EE의 방향에 놓여 회전시켜 object를 움직입니다.

Moving the eye

eye를 옮길 시에는 위와 같은 auxiliary 좌표계에서 eye frame에 affine transform을 직접적으로 적용하여 구현합니다. 단, moving transform을 적용 시에는 inverse를 적용합니다. (EE frame이 view matrix에서 이미 inverse되기 때문입니다.)

Results

해당 프로그램을 구현한 저의 코드는 깃허브에서 확인하실 수 있습니다.

Reference

[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/

profile
Interested in Vision, Generative, Neural Rendering

0개의 댓글