OpenGL Study

OpenGL "Transformations"

lvneux 2022. 11. 22. 14:52

[TRANSFORMATIONS]

1.  transformation

- 이전 장까지 사용했던 object는 정적인 object로, 이번 장에서는 각 frame에서 object의 vertex를 수정하고 buffer를 재구성하여 ㅇㅁ직이게 한다

: 이 과정은 번거롭고 processing power를 많이 요구한다

: object를 변환하기 위해 matrix 객체를 사용한다

 

[Vector]

1. vector

- 가장 기본적 정의로서 vector는 방향이다

: vector는 direction과 magnitude(강도/길이로 알려진 크기)를 가지고 있다

: Ex) 왼쪽으로 10걸음에서, 왼쪽은 vector의 direction이고, 10걸음은 vector에서의 magnitude이다

- 어떠한 차원이든 가질수 있다

: 일반적으로 2~4차원으로 사용한다

- 위 그림에서 vector는 2D 그래프의 (x, y)를 화살표로 나타낸다

: vector를 2D에서 보여주는 것이 3D에 비해 직관적이므로, 2D vector를 z좌표가 0인 3D vector로 생각할 수 있다

: vector는 방향을 나타내므로 원점의 값은 변경하지 않는다

: v¯  w¯ 는 각 vector의 원점이 다름에도 불구하고 같은 vector이다

- vector는 방향으로서 나타내어지기 떄문에 위치로 표현하기 어려운 경우가 있다

: vector를 위치로 시각화하기 위해서는 direction vector의 원점을 (0, 0, 0)으로 설정한 다음, 점을 지정하는 특정 방향을 가리키면 position vector가 된다

: position vector (3, 5)는 그래프에서 (3, 5)를 가리키고 원점은 (0, 0)이다

: vector 이용하여 2D/3D 공간에서의 방향과 위치를 나타낼 수 있다

: 일반적인 수처럼 vector도 연산을 정의할 수 있다

 

 

2. Scalar Vector Operations

- scalar는 하나의 숫자(하나의 요소를 가진 vector)이다

- scalar를 이용해 vector에 대해 사칙연산을 할 때, 간단히 vector의 각 요소들을 scalar로 연산하면 된다

- 위 수식은 각 사칙연산으로 대체하여 사용 가능하나, - 또는 ÷ 의 역순서는 정의되지 않는다

 

3. Vector Negation

- 역벡터는 vector의 반대 방향을 나타낸다

: 북동쪽을 가리키고 있는 vector의 역벡터는 남서쪽을 가리키는 vector이다

- 이를 만들기 위해서는 vector의 각 요소에 - 기호를 붙이면 된다

: 또는 -1 scalar 곱을 해도 된다

 

4. Addition and Subtraction

- 두 vector의 덧셈은 component-wise(요소끼리)로서 정의된다

: vector의 각 요소들과 다른 vector의 각 요소들을 더하는 것을 의미한다

- 일반적인 덧셈, 뺄셈과 마찬가지로 vector의 뺄셈은 두 번째 vector의 역벡터를 더한 것과 같다

- 좌측의 그래프는 v = (4, 2)와 k = (1, 2)의 덧셈을 시각화한 그래프다

- 우측의 그래프는 두 vector의 뺄셈을 시각화한 그래프다

: 두 vector를 서로 뺄셈하여 나온 결과는 두 vector가 가리키는 위치의 차이가 된다

 

5. Length

 

 

- vector의 길이/크기를 구하기 위해 Pythagoras theorem(피타고라스 정리)를 사용한다

: vector의 x와 y 요소를 삼각형의 두 변으로 시각화하면 vector가 삼각형을 형성한다

 

 

- v의 길이를 알아야하므로 피타고라스 정리를 사용한다

-   vector의 길이는 |||| 로 표기된다

: 3D에서는 방정식에 z²를 추가함으로써 확장할 수 있다

: 이 사진에서 (4, 2) vector의 길이는 4.47이다

- uniit vector(단위 벡터)는 그 길이가 정확히 1이라는 특성을 가지고 있다

- vector normalizing (벡터 정규화)

: vector의 각 요소들을 길이로 나누면 어떤 vector로부터도 unit vector를 계산할 수 있다

: 오직 방향만을 고려할 때 유용하게 사용한다 (vector의 크기/길이를 변경해도 방향은 변하지 않음)

 

 

6. Vector-vector Multiplication 

<Dot product_내적>

- 두 vector의 내적은 그 길이의 scalar 곱에 두 vector사이의 각에 cos를 곱한 것과 같다

- 두 벡터 사이의 각은 세타(θ)로 표현된다

: v와 k가 unit vector라면 공식을 단순화할 수 있다

 

- 내적은 오직 두 vector 사이의 각만을 정의한다

: 각도가 0이면 cos는 1을 나타내고, 각도가 90이면 cos는 0을 나타낸다

: 내적을 이용해 두 vector가 직각인지 아닌지, 혹은 평행하는지 아닌지 확인할 수 있다

: unit vector가 아닌 vector 사이의 각을 계산할 때는 그 결과로부터 두 vector의 길이를 나누어야 cosθ만 남는다

- 내적의 곱셈은 요소들끼리 서로 곱하여 값을 구한다

: 두 unit vector의 내적은 위 그림과 같다 (두 vector의 길이는 1이다)

- 두 vector 사이의 각도를 계산하기 위해 cos¹를 사용한다

: 결과 값은 143.1도이다

- 내적은 빛 계산에 유용하게 사용된다

 

<Cross product_외적>

- 외적은 오직 3D 공간에서만 정의되고, 평행하지 않는 두 개의 vector를 입력으로 받으며 두 vector에 직교하는 하나의 vector를 만든다

: 입력된 두 vector가 직교한다면 외적의 결과는 3개의 직교 vector가 된다

: 좌측 그림은 외적이 3D공간에서 어떻게 보여지는지 나타낸 그림이다

- 우측 그림은 직교하는 vector A와 vector B를 외적하는 것에 대한 그림이다

: 이 단계를 거치면 입력된 vector와 직교하는 새로운 vector를 얻을 수 있다

 

[MATRICES]

1. Matrix

- 행렬은 기본적으로 숫자, 기호 수식들의 사각 배열이다

- 행렬은 (i, j)로 인덱싱되며, i는 행이고 j는  열이다

- 2D 그래프를 (x, y)로 인덱싱 할 때와는 정반대로 작용한다

- 행렬은 기본적으로 다진 수학 수식의 사각 배열 그 이상도 이하도 아니다

: 수학적 특성을 가지고 있으며 vector처럼 여러 연산자들을 정의할 수 있다(덧셈, 뺄셈, 곱셈)

 

2. Addition and Subtraction

- 행렬의 덧셈과 뺄셈은 위 그림과 같이 정의된다

: 두 행렬 사이의 덧셈과 뺄셈은 행렬의 요소별로 수행된다

: 일반적인 수와 동일한 규칙이 적용되지만 동일한 인덱스를 가진 두 행렬의 요소에만 적용된다 (같은 차원의 행렬에서만 적용)

: 3x2 행렬과 2x3행렬은 서로 덧셈과 뺄셈을 수행할 수 없다

 

3. Matrix-Scalar products

-  행렬-스칼라 곱셈에서는 행렬의 각 요소들을 scalar로 곱한다

- 위 사진의 숫자(scalar)는 기본적으로 행렬의 모든 요소들의 값을 조정(scales)하므로 scalar라고 불린다

 

4. Matrix-Matrix multiplication

- 행렬 곱셈은 미리 정의된 규칙을 따른다

: 왼쪽 행렬의 열의 개수와 오른쪽 행렬의 행의 개수가 같아야만 곱셈을 수행할 수 있다

: 행렬의 곱셈은 교환법칙이 성립하지 않는다

- 2x2 행렬 곱셈의 계산

: 행과 열의 첫 번째 요소를 시작으로 일반적인 곱셈을 이용하여 결과값을 계산한다

: 결과 행렬은 (왼쪽 행렬의 행 개수, 오른쪽 행렬의 열 개수) 차원으로 나타난다

 

[MATRIX-VECTOR MULTIPLICATION]

1. Vector

- vector는 기본적으로 Nx1차원의 행렬이다

: N은 vector 요소의 개수다 (N-dimensional vector)

: 행렬과 마찬가지로 숫자들의 배열이지만 오직 하나의 열만을 가지고 있다

- 2D/3D 변환들은 행렬 내부에 있고, vector에 그 행렬을 곱하면 vector가 변환된다

 

2. Identity Matrix_단위행렬

- OpenGL에서는 일반적으로 4x4 변환 행렬을 가지고 작업한다

: 대부분의 vector 크기가 4이기 때문이다

- 단위행렬은 NxN 행렬이고, 대각선을 제외하고는 0을 갖는 행렬이다

: 가장 간단한 변환 행렬이다

: vector에 아무런 영향을 끼치지 않는다

- 단위 행렬은 일반적으로 다른 변환 행렬을 생성하는 시작점이고, 선형대수학을 더 깊게 파고들었을 때 정리를 증명하고 방정식을 푸는데 매우 유용한 행렬이라는 점에서 변환하지 않는 변환행렬의 의미를 알 수 있다

 

3. Scaling (크기 조정)

- vector를 scaling할 때, 화살표의 방향은 그대로 둔 채로 원하는만큼 길이를 증가시킨다

: 2, 3차원에서 작업하므로 각각 한 축(x, y or z)을 scaling하는 2, 3개의 scaling 변수의 vector로 scaling을 정의할 수 있다

- v¯ = (3, 2)의 scaling

 

 

: x축을 따라 vector를 0.5배로 조정하여 2배로 좁힌다

: y축을 따라 vector를 2배로 조정하여 2배로 확대한다

: 좌측의 그림은 vector 크기를 (0.5, 2)로 조정한 결과인 s¯ vector를 나타낸다

 

 

 

- OpenGL은 일반적으로 3D 공간에서 동작한다

: 2D 공간을 사용할 경우 z의 값을 1로 설정한다

- scale 연산

: scaling 요소가 각 축마다 같지 않으면 non-uniform scale (비균일 조정)이다

: scalar가 모든 축에서 동일하다면 uniform scale(균일 조정)이다

- scale 변수들을 (S1, S2, S3)처럼 나타낸다면 (x, y, z) vector에 대한 scale행렬을 위 그림과 같이 정의할 수 있다

: 3D 공간이므로 4번째 값은 1로 유지한다 (4번째 값인 w 구성요소는 다른 용도로 사용된다)

 

4. Translation

- Translation은 원본 vector 위에 다른 vector를 더하여 다른 위치의 새로운 vector를 반환하는 작업이다 (vector를 이동)

- scaling과 마찬가지로 4x4 행렬에는 특정 작업을 수행하는 데에 사용할 수 있다

: Translation vector를 (Tx, Ty, Tz) 로 표현한다면 옆 그림과 같이 정의할 수 있다

: 모든 변환 값에 vector의 w열을 곱하고 원래의 값에 더하므로 작동한다 (3x3에서는 불가능하다)

 

< Homogeneous coordinates_동차좌표>

- vector의 w요소는 동차 좌표라고도 불린다

: 동차 좌표로부터 3D vector를 가져오기 위해 x, y, z 좌표를 w 좌표로 나눈다

: 일반적으로 w요소는 1.0이다

- 동차 좌표 생성의 장점

: 3D vector의 이동이 가능하게 한다 (w요소 없이는 vector의 이동이 불가능하다)

+) w 좌표값이 0인 좌표는 이동할 수 없으므로 동차좌표가 0일 경우, vector는 direcction vector라고 불린다 

- 이동 행렬을 사용하여 object를 3가지 방향(x, y, z)으로 이동시킬 수 있다

 

5. Rotation

-  2D, 3D에서의 회전은 각(angle)으로 나타내어진다

: angle은 각도(degree)나 라디안(radian)으로 나타낼 수 있다 (원은 360도와 2PI 라디안으로 나타낼 수 있다)

- 대부분의 회전 함수들은 radian으로 각을 표현한다 (PI = 3.14159265359)

angle in degrees = angle in radians * (180 / PI)
angle in radians = angle in degrees * (PI / 180)

- 반원을 돌리면 180도 회전하고, 1/5을 오른쪽으로 회전하면 72도 회전한다

: vector  가 vector  k¯에서 오른쪽으로 72도 회전하거나 시계 방향으로 회전하는 기본 2D vector에 대해 입증되었다

- 3D 회전은 angle과 회전 축으로 지정된다

: 지정된 angle은 주어진 축을 따라 물체를 회전시킨다

: 3D 공간에서 2D vector를 회전시킬 때 회전축을 z축으로 설정한다

- 삼각법을 사용하면 주어진 각도에서 새로 회전하는 vector로 vector를 변환할 수 있다

: 일반적으로 sin 및 cos 조합을 통해 수행된다

- 회전 행렬은 3D 공간에서 각각의 축에 대해 정의된다 (angle은 θ로 나타내어진다)

- 회전 행렬을 사용하면 위치 vector를 세 가지의 축 중 하나에 대해 변환시킬 수 있다

- 임의의 3D 축을 중심으로 회전하려면 먼저 X축을 중심으로 회전한 후, Y, Z축을 차례로 하여 3개를 모두 결합할 수 있다

: 이 방법은 Gimbal lock 문제를 빠르게 발생시키므로, 일반적으로 회전 행렬을 결합하는 방법 대신 임의의 단위 축(0.662, 0.2, 0.722)을 중심으로 즉시  회전한다

: 위 그림은 (Rx, Ry, Rz)를 회전 축으로 하여 행렬을 표현한 것이다

 

6. Combining Martices

- (x, y, z) vector를 2만큼 scale하고 (1, 2, 3)만큼 이동시키려고 할 때의 결과 변환 행렬이다

: 이동 행렬과 스케일 행렬이 필요하다

- 행렬을 곱할 때에는 먼저 이동을 한 후 scaling을 한다

: 행렬 곱은 교환 법칙이 성립하지 않으므로 순서가 중요하다

: 가장 오른쪽에 있는 행렬이 vector와 처음으로 곱해지므로 곱셈은 오른쪽에서 왼쪽으로 읽어야한다

- 행렬을 조합할 때에는 먼저 scale 연산을 한 후 회전 연산을 하고 마지막에 이동 연산을 한다

: 먼저 이동을 한 후 scaling을 한다면 이동 vector 또한 scale 된다

 

- 변환 행렬을 vector에 적용한 결과이다

- 이 vector는 먼저 2만큼 scale 한 후 (1, 2, 3)만큼 이동한다

 

 

 

[IN PRACTICE]

- OpenGL은 행렬이나 vector 지식에 대한 어떠한 형식도 가지고 있지 않으므로 수학 관련 class와 function을 만들어야한다

: GLM은 OpenGL과 호환되는 수학 라이브러리로, 이를 사용하여 진행한다

 

1. GLM

- GLM

: OpenGL Mathematics의 약자로, header file 전용 라이브러리이다

: 별도의 Linking이나 Compile이 필요하지 않다

: header file의 root 디렉터리를 include 폴더에 복사하고 시작한다

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

- GLM의 대부분의 기능은 위 코드의 header file에서 찾을 수 있다

glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f);
glm::mat4 trans = glm::mat4(1.0f);
trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f));
vec = trans * vec;
std::cout << vec.x << vec.y << vec.z << std::endl;

- (1, 0, 0) vector를 이동하여 (1, 1, 0) vector로 변환하기 (glm::vec4로 정의하면 1.0으로 설정된 정방행렬이 된다)

: GLM의 vector class를 사용하여 vec vector를 선언한다

: mat4(4x4행렬)를 정의하고 대각선을 1.0으로 초기화하여 행렬을 1D 행렬로 명시적으로 초기화한다 (만약 1D 행렬로 초기화하지 않는다면 행렬은 null행렬이 될 것이고, 이후 모든 행렬 연산도 null 행렬로 끝난다)

: 단위행렬을 transformation행렬과 함께 glm::translate 함수로 전달하여 변환 행렬을 만든다

: 변환 vector와 변환 행렬을 곱한 후 결과를 출력한다 (결과 vector는 (1+1, 0+1, 0+0) 연산을 하여 (2, 1, 0) vector가 된다)

glm::mat4 trans = glm::mat4(1.0f);
trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0));
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));

- container object의 크기를 조정하고 회전하기

: container를 각 축에 대해 0.5만큼 scale하고 Z축을 중심으로 90도 회전시킨다

: GLM은 각을 radian으로 받아오므로 glm::radians function을 사용하여 각도를 radian으로 변환시킨다

- rotate = (0.0, 0.0, 1.0) : z축을 기준으로 회전

: 이러한 texture가 입혀진 사각형은 XY 평면이기 때문에 Z축을 중심으로 회전하려고 한다 (회전 축은 단위 벡터여야하므로, X, Y 또는 Z축을 중심으로 회전하지 않을 경우에는 vector가 정규화 되었는지 확인한다)

- scale = (0.5, 0.5, 0.5) : 균등 크기 조절, 2배 작게 조정한다

 GLM의 각 함수에 행렬을 전달하기 때문에 GLM은 행렬을 자동으로 곱하고 모든 변환이 조합된 변환행렬을 return한다

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 TexCoord;
  
uniform mat4 transform;

void main()
{
    gl_Position = transform * vec4(aPos, 1.0f);
    TexCoord = vec2(aTexCoord.x, aTexCoord.y);
}

- shader에서 변환행렬을 가져오는 방법

: mat4 uniform 변수를 받아들일 수 있도록 vertex shader를 적용하고, 위치 vector에 행렬 uniform을 곱한다

- GLSL에는 vector와 swizzling과 같은 연산을 가능하게 하는 mat2와 mat3 type도 있다

: 모든 수학 연산(scalar*matrix, matrix*vector, matrix*matrix 등)은 행렬 type위에서 가능하다

unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));

- uniform 설정

: gl_Position에 전달하기 전에 변환 행렬과 위치 vector를 곱해주었다

: container가 2배로 작고 90도 회전(왼쪽으로) 해야하므로 변환 행렬을 shader에 전달해야한다

-  glUniformMatrix4fv(location, matrix의 개수, column-major ordering, 실제 행렬 데이터)

: 행렬 데이터를 shader에 보낸다

: column-major ordering은 GLM의 기본 행렬 layout인 내부행렬 layout을 사용한다 (행과 열을 바꿀 필요가 없으면 false로 지정한다)

: 실제 행렬 데이터는 value_ptr function을 사용하여 행렬을 변환해 type을 맞춘다

+) reinterpret_cast<float*>(&trans) == glm::value_ptr(trans) == &trans[0][0]

: reinterpret_cast는 모든 pointer type 간의 형 변환을 허용한다 (static_cast는 오직 상속 관계의 pointer끼리만, compile 시간에 캐스팅 완료)

: 이전 값에 대한 바이너리를 유지 (type에 따라 출력되는 값이 다를 수 ㅇ있음)

 

2. Result

 

3. GLM_Additionally

glm::mat4 trans = glm::mat4(1.0f);
trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f));
trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));

- 시간에 따라 계속 회전하고 container를 window의 우측 하단으로 위치시키기

: loop안에서 변환 행렬을 계속 수정해야한다

: 시간에 따른 각을 얻기 위해 GLFW의 시간 함수를 사용한다

: 일반적으로 화면을 render할 때 loop가 돌 때마다 새로운 값으로 재생성된 여러 변환 해렬들을 사용합니다

- container를 (0, 0, 0)을 중심으로 회전시키고 회전된 container를 화면 우측 하단으로 이동시킨다

: code에서는 이동 후에 회전을 시켰지만, 실제 변환 순서는 거꾸로 진행된다

+) vertex shader에서 변환을 사용하면 데이터를 계속해서 다시 전달할 필요가 없기 때문에 vertex 데이터를 다시 정의하는 수고를 덜어주고 작업 시간을 아낄 수 있다

 

[CODE]

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#include <learnopengl/shader_s.h>

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // build and compile our shader zprogram
    // ------------------------------------
    Shader ourShader("5.1.transform.vs", "5.1.transform.fs");

    // set up vertex data (and buffer(s)) and configure vertex attributes
    // ------------------------------------------------------------------
    float vertices[] = {
        // positions          // texture coords
         0.5f,  0.5f, 0.0f,   1.0f, 1.0f, // top right
         0.5f, -0.5f, 0.0f,   1.0f, 0.0f, // bottom right
        -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, // bottom left
        -0.5f,  0.5f, 0.0f,   0.0f, 1.0f  // top left 
    };
    unsigned int indices[] = {
        0, 1, 3, // first triangle
        1, 2, 3  // second triangle
    };
    unsigned int VBO, VAO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);

    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    // position attribute
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // texture coord attribute
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);


    // load and create a texture 
    // -------------------------
    unsigned int texture1, texture2;
    // texture 1
    // ---------
    glGenTextures(1, &texture1);
    glBindTexture(GL_TEXTURE_2D, texture1);
    // set the texture wrapping parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    // set texture filtering parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // load image, create texture and generate mipmaps
    int width, height, nrChannels;
    stbi_set_flip_vertically_on_load(true); // tell stb_image.h to flip loaded texture's on the y-axis.
    unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);
    // texture 2
    // ---------
    glGenTextures(1, &texture2);
    glBindTexture(GL_TEXTURE_2D, texture2);
    // set the texture wrapping parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    // set texture filtering parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // load image, create texture and generate mipmaps
    data = stbi_load("awesomeface.png", &width, &height, &nrChannels, 0);
    if (data)
    {
        // note that the awesomeface.png has transparency and thus an alpha channel, so make sure to tell OpenGL the data type is of GL_RGBA
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);

    // tell opengl for each sampler to which texture unit it belongs to (only has to be done once)
    // -------------------------------------------------------------------------------------------
    ourShader.use();
    ourShader.setInt("texture1", 0);
    ourShader.setInt("texture2", 1);


    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.46667f, 0.533333f, 0.6f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // bind textures on corresponding texture units
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture1);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, texture2);

        // create transformations
        glm::mat4 transform = glm::mat4(1.0f); // make sure to initialize matrix to identity matrix first
        transform = glm::translate(transform, glm::vec3(0.5f, -0.5f, 0.0f));
        transform = glm::rotate(transform, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));

        // get matrix's uniform location and set matrix
        ourShader.use();
        unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
        glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(transform));

        // render container
        glBindVertexArray(VAO);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // optional: de-allocate all resources once they've outlived their purpose:
    // ------------------------------------------------------------------------
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);

    // glfw: terminate, clearing all previously allocated GLFW resources.
    // ------------------------------------------------------------------
    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow* window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}