눈팅하는 게임개발자 블로그

OpenGL Lighting 3-2 Basic Lighting 본문

공부한거/OpenGL

OpenGL Lighting 3-2 Basic Lighting

Palamore 2020. 10. 11. 16:00

원문 사이트

learnopengl.com/Lighting/Basic-Lighting

 

LearnOpenGL - Basic Lighting

Basic Lighting Lighting/Basic-Lighting Lighting in the real world is extremely complicated and depends on way too many factors, something we can't afford to calculate on the limited processing power we have. Lighting in OpenGL is therefore based on approxi

learnopengl.com

번역 사이트

heinleinsgame.tistory.com/15

 

[Learn OpenGL 번역] 3-2. 조명 - 기본 조명

기본 조명 조명/기본 조명 실생활의 조명은 매우 복잡하고 프로세싱 파워로 계산하기 어려울 정도의 많은 요소들로 이루어져 있습니다. 그러므로 OpenGL에서의 조명은 처리되기 쉽고 실세계의 사

heinleinsgame.tistory.com

 

Basic Lighting

실제 세계의 조명은 매우 복잡하고 컴퓨터의 프로세싱 파워로 계산하기 어려울 정도의 많은 요소들로 이루어져 있다.

컴퓨터 그래픽스에서는 실세계의 사물과 비슷하게 보이는 모델을 사용하여 실세계에 대한 근사치를 기반으로 조명을 처리한다.

조명 모델들은 빛의 물리학을 기반으로 이루어져 있는데.

이 모델들 중 하나인 Phong Lighting Model은 3가지의 요소로 이루어져 있다.

ambient lighting(주변광), diffuse lighting(분산광), specular lighting(반사광)

다음 이미지에서 해당 조명 요소들이 실제로 어떻게 보이는지 확인할 수 있다.

Ambient Lighting : 언제든 어두울 때도 일반적으로 world의 어딘가에는 조명이 존재(달 또는 멀리있는 조명)하므로

오브젝트는 대부분 완전히 어두워지지는 않는다. 이를 시뮬레이션하기 위해 오브젝트에 특정 색상을 주는 ambient 조명

상수를 사용한다.

Diffuse Lighting : 조명 오브젝트가 가지고 있는 방향이 있는 조명을 오브젝트에 비추어 시뮬레이션한다.

이는 조명 모델에서 시각적으로 가장 중요한 요소이다.

오브젝트의 많은 부분이 광원을 마주보고있을수록 더 밝아진다.

Specular Lighting : 빛나는 오브젝트에서 볼 수 있는 밝은 지점을 시뮬레이션 한다.

반사 하이라이트는 종종 오브젝트의 색상보다는 조명의 색상에 더 치우쳐진다.

 

시각적으로 봐줄만 한 Scene을 생성하기 위해 최소한 이 3가지의 조명 요소들을 시뮬레이션해야 한다.

가장 간단한 ambient Lighting부터 시작해보자.

 

Ambient Lighting

빛은 일반적으로 하나의 광원이 아닌 주변에 산재한 많은 광원들로부터 물체에 도착한다.(그 광원들이 직접적으로 보이지 않더라도)

빛의 특성상 근처에 있지 않은 지점에 도달하기 위해 여러 방향으로 퍼지고 산란하기 때문에

빛은 면에서 반사될 수 있고 결과적으로 오브젝트에 간접적으로 영향을 준다.

이를 고려한 알고리즘을 global illumination 알고리즘이라고 하고 이는 비용이 많이 들고 매우 복잡하다.

 

ambient lighting이 바로 global illumination 알고리즘의 아주 간단한 모델이다.

위에서 미리 언급하였듯이 오브젝트 fragment의 최종 색상에 추가할 작은 상수 색상을 사용한다.

따라서 직접적인 광원이 없다고 하더라도 항상 퍼지는 빛이 있는 것처럼 보이게 된다.

 

Scene에 ambient lighting을 추가하는 것은 어렵지 않다.

빛의 색상을 정하고 이를 작은 상수 ambient 요소와 곱한다.

그리고 이를 오브젝트의 색상과 곱하여 fragment의 색상으로 이 값을 사용한다.

void main()
{
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * lightColor;

    vec3 result = ambient * objectColor;
    FragColor = vec4(result, 1.0);
}  

 

이제 프로그램을 실행시키면 조명의 첫 번째 단계(ambient lighting)이 성공적으로 적용되었음을 확인할 수 있다.

상당히 어두운 색상이지만 ambient lighting이 적용되었기 때문에 완전히 어둡지는 않다.

결과는 다음과 같다.

 

Diffuse Lighting

Ambient Lighting은 그 자체로는 유의미한 결과를 만들지 못한다.

하지만 Diffuse Lighting은 오브젝트에 중요한 시각적인 효과를 주기 시작한다.

Diffuse Lighting은 해당 오브젝트의 광선에 정렬된 fragment가 광원과 가까이 있을수록 해당 오브젝트가 밝아진다.

오브젝트의 하나의 fragmnet를 향해 광선을 쏘고 있는 광원이 왼쪽에 존재한다.

연산을 위해서 광선과 fragment 사이의 각이 필요한데,

광선이 오브젝트의 면에 수직으로 향한다면 빛은 아주 많은 영향을 끼칠 것이다.

광선과 fragment 사이의 각을 측정하기 위해서는 법선 벡터(Normal Vector)라고 불리는 것을 사용한다.

법선 벡터는 fragment 면에 대해 수직인 벡터이다.(노란색으로 표현된 벡터)

두 벡터 사이의 각은 내적을 통해 쉽게 구할 수 있다.

 

여기서 두 유닛 벡터 사이의 각이 작을수록 내적은 1과 가까워진다는 사실을 이미 알고 있고.

두 벡터 사이의 각이 직각일 때 내적은 0이 된다는 사실 또한 알고 있다.

따라서 두 벡터 사이의 각이 클수록 빛의 영향을 더 적게 받게 된다.

여기서 내적의 결과로 fragment의 색상에 대한 빛의 영향을 계산하기 위해 사용할 스칼라를 얻을 수 있다.

결과적으로 빛을 향한 방향에 따라 다르게 빛나는 fragment를 만들 수 있다.

diffuse lighting을 계산하기 위해 필요한 것은 다음과 같다.

Normal Vector(법선 벡터) : vertex의 면에 수직인 벡터.

The directed light ray(방향을 가진 광선) : 빛의 위치에서 fragment위치로 향하는 방향 벡터이다.

이 광선을 계산하기 위해 빛의 위치 벡터와 fragment의 위치 벡터가 필요하다.

 

Normal Vector

법선 벡터는 vertex의 면에 수직인 (단위)벡터이다.

vertex는 그 자체로 면을 가지고 있지 않기 때문에(단지 공간 내부에 하나의 점일 뿐)

vertex의 면을 알아내기 위해 주변의 vertex들을 사용하여 법선 벡터를 구한다.

큐브의 모든 vertex들에 대한 법선 벡터를 계산하기 위해 외적을 사용하여 약간의 트릭을 사용할 수 있다.

하지만 3D 큐브는 복잡한 도형이 아니기 때문에 간단히 법선 벡터를 vertex data에 수작업으로 넣을 수도 있다.

수작업으로 수정된 vertex data는 다음과 같다.

float vertices[] = {
    -0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
     0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f, 
     0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f, 
     0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f, 
    -0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f, 
    -0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f, 

    -0.5f, -0.5f,  0.5f,  0.0f,  0.0f, 1.0f,
     0.5f, -0.5f,  0.5f,  0.0f,  0.0f, 1.0f,
     0.5f,  0.5f,  0.5f,  0.0f,  0.0f, 1.0f,
     0.5f,  0.5f,  0.5f,  0.0f,  0.0f, 1.0f,
    -0.5f,  0.5f,  0.5f,  0.0f,  0.0f, 1.0f,
    -0.5f, -0.5f,  0.5f,  0.0f,  0.0f, 1.0f,

    -0.5f,  0.5f,  0.5f, -1.0f,  0.0f,  0.0f,
    -0.5f,  0.5f, -0.5f, -1.0f,  0.0f,  0.0f,
    -0.5f, -0.5f, -0.5f, -1.0f,  0.0f,  0.0f,
    -0.5f, -0.5f, -0.5f, -1.0f,  0.0f,  0.0f,
    -0.5f, -0.5f,  0.5f, -1.0f,  0.0f,  0.0f,
    -0.5f,  0.5f,  0.5f, -1.0f,  0.0f,  0.0f,

     0.5f,  0.5f,  0.5f,  1.0f,  0.0f,  0.0f,
     0.5f,  0.5f, -0.5f,  1.0f,  0.0f,  0.0f,
     0.5f, -0.5f, -0.5f,  1.0f,  0.0f,  0.0f,
     0.5f, -0.5f, -0.5f,  1.0f,  0.0f,  0.0f,
     0.5f, -0.5f,  0.5f,  1.0f,  0.0f,  0.0f,
     0.5f,  0.5f,  0.5f,  1.0f,  0.0f,  0.0f,

    -0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,
     0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,
     0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,
     0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,
    -0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,
    -0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,

    -0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f,
     0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f,
     0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,
     0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,
    -0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,
    -0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f
};

vertex 배열에 데이터를 추가했기 때문에 lighting vertex shader를 수정해야 한다.

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
...

이제 각 vertex들에 법선 벡터를 추가하였고 vertex shader를 수정하고 vertex attribute Pointer 또한 수정하였다.

광원 큐브는 같은 vertex배열의 데이터를 사용하지만 lightingcube shader에서는 새롭게 추가된 법선 벡터를

사용하지 않는다.

shader나 attribute 구성을 수정할 필요가 없고 vertex attribute pointer에 새로운 vertex배열의 크기를 반영한다.

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

 

모든 빛에 대한 계산은 fragment shader에서 완료되므로 vertex shader의 법선 벡터를 fragment shader에 전달해야 한다.

out vec3 Normal;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    Normal = aNormal;
} 

그리고 이를 fragment shader에서 입력 변수로 받아준다.

in vec3 Normal;  

 

Diffuse Color 계산

이제 각각의 vertex에 대한 법선 벡터를 가지고 있고 이제는 fragment 위치 벡터와 빛의 위치 벡터가 필요하다.

빛의 위치는 하나의 정적인 변수이기 때문에 fragment shader에 uniform으로 간단하게 전해줄 수 있다.

uniform vec3 lightPos;  

다음 게임 루프 안에서 uniform을 업데이트해준다.(광원의 위치가 바뀌지 않는다면 게임루프 밖에서)

위치로서 미리 선언해둔 lightPos 벡터를 사용한다.

lightingShader.setVec3("lightPos", lightPos);  

 

이제 마지막으로 필요한 하나는 실제 fragment의 위치이다.

모든 빛에 대한 계산은 world space에서 할 것이므로 world space에 있는 vertex의 위치가 필요하다.

이는 vertex 위치 attribute를 오직 model 행렬과 곱하여 world space 좌표로 변환하는 것으로 수행될 수 있다.

이는 verte xshader에서 쉽게 수행될 수 있으므로 출력 변수를 선언하고 world space 좌표를 계산해본다.

out vec3 FragPos;  
out vec3 Normal;
  
void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = aNormal;
}

그리고 해당 변수를 fragment shader에 입력변수로 추가해준다.

in vec3 FragPos; 

 

이제 필요한 모든 변수들이 설정되었고 fragment shader에서 빛에 대한 계산을 시작할 수 있다.

계산을 하기 위해 먼저 필요한 것은 광원과 fragment의 위치 사이의 방향 벡터이다.

빛의 방향 벡터는 빛의 위치 벡터와 fragment의 위치 벡터를 뺄셈 연산을 해줌으로써 쉽게 얻을 수 있다.

vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);  

항상 벡터를 정규화할 것을 명심하고.

 

다음 norm과 lightDir 벡터를 내적하여 실제 diffuse 효과를 계산한다.

그러면 diffuse 요소를 얻기 위해 결과 값은 빛의 색상과 곱해진다.

최종적으로 두 벡터 사이의 각이 클수록 diffuse의 요소는 어두워진다.

float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;

두 개의 벡터 사이의 각이 90도 보다 크다면 내적의 결과는 음수가 되고 음의 diffuse 요소를 가지게 된다.

이러한 이유 때문에 max 함수를 사용해서 diffuse 요소가 음의 값을 가질 수 없도록 한다.

빛에서 음의 값을 가지는 색상은 실제로 정의되지 않으므로 이는 피하는 것이 좋다.

 

이제 ambient와 diffuse 컴포넌트를 가지고 있고, 이 두개의 컴포넌트에 설정된 색상과 오브젝트의 색상을 곱하여

최종 fragment의 컬러를 결정한다.

vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);

 

성공적으로 컴파일을 수행했다면 결과는 다음과 같다.

 

diffuse Lighting과 함께 큐브가 실제 큐브처럼 보이는 것을 알 수 있다.

 

One Last thing

현재로서는 법선 벡터를 그냥 그대로 vertex shader에서 fragment shader로 보내기만 한다.

하지만 fragment shader에서 수행해왔던 계산들은 모두 world space 좌표에서 수행되므로

법선 벡터를 world sapce 좌표로 변환해야 한다.

이는 단순히 model 행렬과 곱하는 것이 아니라서 다소 복잡할 수 있다.

 

제일 먼저 법선 벡터는 오직 방향 벡터이다. 공간에서 특정한 위치를 나타내지 않는다.

또한 법선 벡터는 동차 좌표(위치 벡터의 w 요소)를 가지고 있지 않다.

이는 이동 변환은 법선 벡터에서 아무런 효과를 가지지 않는다는 것을 의미한다.

그리고 법선 벡터에 model 행렬을 곱하고 싶다면 model 행렬의 좌측 상단의 3x3 행렬을 취하여

이동 행렬의 일부를 지워야한다. 

법선 벡터에 적용할 변환은 스케일 변환과 회전 변환이다.

 

두 번째로 model 행렬이 불균일 스케일을 수행하면 vertex들이 수정되어 법선 벡터가 더이상 해당 면과 수직이

되지 않는다.

그래서 해당 model 행렬로 법선 벡터를 변환할 수 없다.

다음 이미지는 불균일 스케일을 수행하는 model 행렬로 법선 벡터를 변환했을 때의 이미지이다.

불균일 스케일을 적용할 때마다 법선 벡터는 해당 면에 수직하지 않게 되어 빛을 왜곡하게 된다.

 

이를 해결하는 트릭은 특별히 법선 벡터에 맞춰서 만들어진 다른 model 행렬을 사용하는 것이다.

이 행렬은 법선 행렬이라고 불리며 법선 벡터를 잘못된 형태로 스케일링하는 효과를 지워주는

선형대수학 연산들을 사용한다.

 

이 법선 행렬은 'model 행렬의 좌측 상단 모서리의 역행렬에 대한 전치행렬'로 정의될 수 있다.

대부분의 자료에서 법선 행렬을 위의 연산들을 model-view 행렬에 적용하는 것으로 정의한다.

하지만 여기서는 world space(view space가 아닌)에서 작업하기 때문에 오직 model 행렬만을 사용한다.

 

vertex shader에서 모든 행렬 유형에서 작동하는 inverse, transpose 함수를 사용하여 스스로 법선 행렬을

생성할 수 있다.

또한 행렬을 3x3 행렬로 변환하므로 이동 속성을 잃지만 vec3법선 벡터와 곱셈을 할 수 있게 되었다.

Normal = mat3(transpose(inverse(model))) * aNormal;  
더보기

역행렬로 변환하는 것은 비용이 많이 드는 연산이기 때문에 해당 연산을 피하도록 노력해야 한다.

공부 목적으로는 문제가 없지만 효율적인 응용 프로그램을 위해서는 CPU에서 법선 행렬을 계산하고 이것을

렌더링하기 전에 uniform을 통해 shader로 전달해야 한다.

오브젝트에 어떠한 스케일 연산도 수행하지 않았기 때문에 실제 법선 행렬을 사용할 필요가 없었고.

법선을 모델 행렬과 곱하기만 해줌으로써 완성되었다.

하지만 여기서 불균일 스케일을 수행한다면 빛이 왜곡되어 보일 것이므로

법선 벡터에 법선 행렬을 반드시 곱해주어야 한다.

 

Specular Lighting

이제 specular 하이라이트를 추가하여 Phong Lighting Model을 끝낼 수 있다.

Diffuse Lighting과 마찬가지로 specular lighting은 빛의 방향 벡터와 오브젝트의 법선 벡터를 기반으로 하지만

플레이어가 fragment를 바라보고 있는 방향에 대한 view 방향도 기반으로 해야 한다.

specular lighting은 빛의 반사 특성을 기반으로 한다.

오브젝트의 면을 거울이라고 생각하면 이 면에서 반사되어진 빛을 보는 specular lighting은 가장 센 빛일 것이다.

다음의 그림에서 해당 효과를 확인할 수 있다.

법선 벡터 주위로 빛의 방향을 반사하여 반사 벡터를 계산한다.

다음 이 반사 벡터와 view 방향 사이의 거리를 계삲나다.

두 벡터 사이의 각이 가까울수록 specular light의 강도는 강해진다.

최종 효과는 오브젝트를 통해 반사된 빛의 방향을 바라볼 때 약간의 하이라이트를 보는 것이다.

 

View 벡터는 specular lighting에 필요한 하나의 추가적인 변수이다.

viewer의 world space 위치와 fragment들의 위치를 사용하여 이 변수를 구할 수 있다.

다음 specular의 세기를 계산하고 이를 빛의 컬러와 곱하고 이를 ambient, diffuse 요소에 추가한다.

 

Viewer의 world space 좌표를 얻기 위해서 간단히 카메라 오브젝트의 위치 벡터를 사용하면 된다.

이제 fragment shader에 다른 uniform을 추가하고 해당 카메라 위치 벡터를 fragment shader에 전달한다.

uniform vec3 viewPos;
lightingShader.setVec3("viewPos", camera.Position); 

 

이제 필요한 모든 변수를 가졌으므로 specular 세기를 계산할 수 있다.

먼저 specular 하이라이트에 효과가 너무 세지 않을 정도의 중간 밝기의 색상으로 specular 세기 값을 정의한다.

float specularStrength = 0.5;

이를 1.0으로 설정하면 아주 밝은 요소를 얻을 수 있고 이는 coral 큐브에 대해 지나치게 강한 효과를 갖는다.

다음 view 방향 벡터와 해당 반사 벡터를 계산한다.

vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);  

 

lightDir 벡터의 부호를 마이너스로 바꿨다는 것을 알아두어야 한다.

reflect 함수는 첫 번째 인자(벡터)로 광원으로부터 fragment 위치로 향하는 벡터를 받는데

lightDir 벡터는 현재 fragment에서 광원으로 향하는 벡터이다.

정확한 reflect 벡터를 얻기 위해 먼저 lightDir 벡터의 부호를 반대로 할 필요가 있다.

두 번째 파라미터는 법선 벡터를 받으므로 정규화된 norm 벡터를 넘겨준다.

 

다음과 같은 공식으로 실제로 specular 컴포넌트를 계산한다.

float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;  

먼저 view 방향과 reflect 방향을 내적한다.(그리고 max 함수를 활용해 음수가 되지 않도록 한다)

그 다음 이를 32제곱 해준다.

이 32값은 하이라이트의 shineiness(빛남의 정도)이다.

오브젝트의 shineiness 값이 높을 수록 빛을 주변에 퍼지게 하지 않고 적절하게 반사한다.

따라서 하이라이트가 작아진다.

다음 이미지는 shininess 값에 따른 시각적 효과를 보여준다.

 

specular 컴포넌트가 너무 많은 값을 차지하게 하지 않기 위해서 32를 사용한다.

이제 해당 컴포넌트를 ambient와 diffuse 컴포넌트에 추가해주고 오브젝트 색상의 결과값에 곱하여 혼합한다.

vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);

 

결과는 다음과 같다.

더보기

lighting shader를 적용하는 초창기에 개발자들은 vertex shader에서 Phong Lighting model을 구현하고는 했었다.

이를 Gouraud Shading이라고 불렀다.

이는 fragment 보다 vertex의 수가 적으므로 총 lighting계산에 사용되는 비용이 적게 든다는 장점이 있지만

vertex shader의 최종 색상값은 오직 vertex만의 lighting 색상이고, 이 상황에서는

fragment가 색상 값을 보간하여 다음과 같은 결과가 나오게 된다.

보간 때문에 lighting의 성능이 떨어지는 것을 확인할 수 있다.

 

 

'공부한거 > OpenGL' 카테고리의 다른 글

OpenGL Lighting 3-4 Lighting maps  (0) 2020.10.22
OpenGL Lighting 3-3 Materials  (0) 2020.10.22
OpenGL Lighting 3-1 Color  (0) 2020.10.11
OpenGL Getting Started 2-9 Camera  (0) 2020.09.29
OpenGL Getting Started 2-8 Coordinate System  (0) 2020.09.29