Home [learn-opengl] Lighting: Basic Lighting
Post
Cancel

[learn-opengl] Lighting: Basic Lighting

Basic Lighting


  • 실생활의 조명: 매우 복잡
  • OpenGL의 조명: 처리되기 쉽고, 실세계의 사물과 비슷하게 보이는 모델을 사용, 실세계에 대한 근사치 기반

    • Phong Lighting model이 그 중 하나.
      • Phong 모델은 3가지의 요소로 이루어짐
      • ambient lighting(주변광): 조명이 어딘가에 있으면, 오브젝트는 완전히 어두워지지 않음. 이를 시뮬레이션하기 위해 항상 객체에 약간의 색상을 주는 주변 조명 상수가 필요
      • diffuse lighting(분산광): 밝은 물체가 물체에 미치는 방향 충돌을 시뮬레이션한다. 조명 모델에서 가장 시각적으로 중요한 구성요소. 물체의 일부가 광원을 향할수록 밝아진다.
      • specular lighting(반사광): 반짝이는 물체에 나타나는 빛의 밝은 부분을 시뮬레이션한다. 반사 하이라이트=> 객체의 색상보다 빛의 색상에 가깝
  • Phong모델 이 3가지 요소들을 시뮬레이션해야함

Ambient lighting

  • 빛은 하나의 광원이 아니라, 많은 광원들로부터 온다.
    • 빛의 특성 중 하나: 어떠한 지점에 도달하기 위해 여러 방향으로 퍼지고 산란
    • 빛 == 면에서 반사되어 어떠한 물체의 간접적인 영향을 준다.
      • 이를 고려한 알고리즘: global ilumination알고리즘
      • 이 알고리즘은 비용이 비싸며 매우 복잡함.
  • global ilumination 알고리즘보다 간단한 모델을 사용할것임.
    • 이를 ambient lighting라고 부름
  • ambient lighting 추가
    • 빛의 컬러 정함
    • 작은 상수 ambient 요소와 곱함.
    • 이를 오브젝트의 컬러와 곱하여 fragment의 컬러로 사용
1
2
3
4
5
6
7
8
void main()
{
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * lightColor;

    vec3 result = ambient * objectColor;
    FragColor = vec4(result, 1.0);
}
  • 이제 프로그램을 실행시키면, 조명의 첫 번째 단계가 적용된것을 확인 가능
    • 이 오브젝트는 거의 안보이지만, 완전히 안보이지는 않다.(조명램프는 다른 shader 사용하기 때문에 영향없음)

Diffuse Lighting

  • 분산광은 광원에서 오는 광선에 오브젝트의 fragment가 더 가깝게 위치할수록 오브젝트의 밝기가 높아진다.

  • 오브젝트 하나의 fragment에 광선과 fragment 사이의 각이 필요하다.
    • 광선이 오브젝트의 면에 수직으로 향한다면 빛은 아주 많이 영향을 끼칠것이다.
    • 광선과 fragment 사이의 각을 측정하기 위해서는 법선 벡터(normal vector)라고 불리는 것을 사용한다.
      • 법선 벡터: fragment 면에 대해 수직인 벡터이다.(위 이미지에서 노란 화살표)
  • 사이각은 내적을 통해 얻을 수 있다.
    • 두 벡터 사이의 각이 90일 경우, 내적은 0이된다.
    • 두 벡터 사이의 각 theta의 값이 크면, 빛의 영향을 더 적게 받게됨.

      두 벡터 사이의 코사인을 얻기 위해 정규화된 벡터를 사용해야한다.

  • 내적 결과값(스칼라)을 fragment의 색상에 미치는 빛의 영향을 게산하는데 사용할 수 있다.

  • 즉, diffuse lighting을 계산하기 위해 Normal vector, The directed light ray 가 필요하다.
    • 레이를 계산하기 위해 빛의 위치 벡터와 fragment의 위치 벡터가 필요하다.

Normal vectors

  • 법선벡터
    • vertex는 단지 점
    • vertex의 면을 알아내기 위해 주변의 vertex들을 사용하여 벡터를 구해야한다.
    • 간단한 큐브이므로, 수작업으로 추가 가능하다.
      • 외적을 사용할 수 있다고한다.
    • 수정된 vertex data
  • vertex 배열에 데이터를 추가했으므로 lighting vertex shader를 수정해야함.
1
2
3
4
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
//...
  • 램프는 같은 vertex data를 사용하지만, 추가된 법선 벡터를 사용하지 않음.

    • 그냥 vertex atribute pointer에 vertex 배열 크기를 반영해야함.(3개의 데이터만 사용함, 그러므로 노멀벡터는 뛰어넘기위해 6을 stride에 곱해줌)
    • 이런 vertex 데이터는 이미 GPU메모리 안에 저장되어 있기 때문에, GPU 메모리에 새로운 데이터를 저장할 필요가 없다. (새로운 VBO 할당보다 효율적)
    1
    2
    
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    
  • 모든 빛 계산은 fragment shader에서 완료되므로, 법선벡터를 전달해야함

1
2
3
4
5
6
7
8
out vec3 Normal;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    Normal = aNormal;
}
  • fragment shader에 Normal을 입력변수로 선언해야함.
1
2
in vec3 Normal;

Caculating the diffuse color

  • fragment 위치 벡터와 광원의 위치벡터가 필요하다.

    • 광원의 위치 == 하나의 정적인 변수 => uniform으로 간단히 선언 가능.
    1
    2
    
    uniform vec3 lightPos;
    
  • 게임 루프 안에서 uniform을 업데이트(또는 밖에서 광원위치 고정)

1
2
lightingShader.setVec3("lightPos", lightPos);
  • 그 다음은 fragment의 위치를 얻어야한다.
    • 모든 빛에 대한 계산을 world space에서 할것이므로 world space에서의 vertex 위치가 필요하다.
    • vertex 위치는 model 행렬과 곱하여 world space 좌표로 변환하는 것으로 얻을 수 있음
    • 이는 vertex shader에서 쉽게 수행가능, 출력 변수를 선언하고, world space 좌표를 계산해야한다.
1
2
3
4
5
6
7
8
9
10
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에 추가해야한다.
1
2
in vec3 FragPos;
  • 이제 fragment shader에서 빛에 대한 계산을 시작해야한다.

    • 먼저 광원과 fragment의 위치 사이의 방향 벡터가 필요
      • 벡터 뺄셈으로 계산
    • 그리고 모든 벡터들이 단위 벡터로 정규화해야한다.
    1
    2
    3
    
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    
  • 빛계산은 일반적으로 벡터의 크기에 대해 생각하지 않음.
    • 방향만 생각(단위 벡터)
    • 계산 간단해짐
  • norm, lightDir 를 내적 => diffuse 효과를 계산
    • diffuse : 내적의 결과값과 lightColor
    • 사이각이 클수록 diffuse 요소는 어두워짐
      1
      2
      
      float diff = max(dot(norm, lightDir), 0.0);
      vec3 diffuse = diff * lightColor;
      
  • 음의 값을 가진 컬러가 되는것을 방지하기 위해 max 함수를 사용한다.

  • 이제 ambient와 diffuse 두 색상을 모두 더한 다음 결과에 객체의 색상을 곱해 결과로 생성된 fragment의 출력색상을 얻는다.
1
2
3
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);
  • 결과는 다음과 같다.

  • 사이 각이 클수록 fragment는 어두워진다.

전체 코드

One last thing

  • 법선 벡터를 그대로 shader에 보내기만 했음.

    • fragment shader에서 수행한 계산들 모두 world space에서 수행되므로 법선 또한 world space에서 수행되어어야 하는게 맞음
    • 하지만, model 행렬과 곱하는 것만큼 간단하지 않다.
  • 간단하지 않은 이유

  1. 법선 벡터는 그냥 방향벡터이다.
    • 특정한 위치를 나타내지 않음.
    • 또한 동차좌표(위치 벡터의 w요소)를 가지고 있지 않는다. => 변환행렬과 곱하기 힘듬.
    • model 행렬을 곱하려면 일부분 제거해 좌측상단의 3x3행렬을 취해야함
    • 즉 변환행렬에서 유효한것은 회전 및 스케일 변환이다.
  2. model 행렬이 불균일 스케일을 수행하면, vertex들이 수정되어 법선 벡터가 더이상 해당 면과 수직이 되지 않는다. - 그러므로 이 model행렬로 법선 벡터를 변환하지 못함. (균일 스케일은 단지 크기만 변하기 때문에 법선에 영향안준다)
  • 불균일 스케일을 적용 => 수직이 틀어짐 => 빛을 왜곡하게됨

  • 해결방법은 법선 벡터에 맞춰서 만들어진 다른 model 행렬을 사용하는것.
  • 법선 행렬: model 행렬의 좌측 상단 모서리의 역행렬에 대한 전치행렬

    • 대부분 자료들은 법선 행렬을 model-view 행렬에서 파생된것으로 사용하지만, 지금은 world space에서만 작업하기 때문에 model 행렬에서 파생시킨것을 사용해야한다.
  • vertex shader에서 inverse, transpose함수를 사용하여 법선 행렬을 만들 수 있다.
    • 법선벡터와 곱하기위해 3x3행렬로 변환하므로 translation 속성을 잃는것을 주의하자.
1
2
Normal = mat3(transpose(inverse(model))) * aNormal;
  • 스케일 연산을 수행하지 않으므로, 실제로는 법선 행렬을 사용할 필요가 없고, 법선을 모델행렬과 곱하기만하면됨.

  • 역행렬 변환 == 비용이 비쌈.
    • scene의 각각의 vertex에 대해 수행해야하므로 이 연산은 피하는게 좋음
  • 보통 CPU에서 법선 행렬을 계산하고 uniform을 통해 shader에 전달함.

Specular Lighting

  • 반사광은 분산광처럼 조명의 방향 벡터 및 대상의 법선 벡터를 기반으로함.

    • 하지만, 추가적으로 플레이어가 fragment를 보고 있는 방향에 대한 view 방향도 관여한다.
  • 반사광은 빛의 반사 특성 기반임.

    • 만약, 거울면에서 반사되어진 빛 즉, 반사광은 가장 밝은 빛일것이다.(아래 그림 참고)

  • 먼저, 반사 벡터를 계산해야함.

    • 그리고 반사벡터와 view 방향 사이의 각도를 계산해야함
    • 각이 가까움: 반사광의 강도는 강해짐
  • 그다음에, View 벡터 계산해야함.

    • viewer의 world sapce 위치와 fragment들의 위치를 사용하여 이 변수를 계산할 수 있다.
  • 마지막으로, specular 세기를 계산하고, 빛의 색과 곱하고, 이를 ambient, diffuse 에 추가한다.

이때까지 world space에서 lighthing 계산을 했음. 하지만 대부분 view space 에서 lighting 을 수행함. view space에서 계산을 하면, viewer 위치가 항상 (0, 0, 0)이므로, 항상 뷰어의 위치를 쉽게 알 수 있음. 여기서는 학습목적으로 worldspace에서 계산할것임. view space에서 계산하려면 관련된 모든 벡터를 view 행렬을 사용하여 변환해야함.(법선 행렬 포함)

  • viewer의 world space 좌표를 얻기 위해 카메라 객체의 위치 벡터를 사용함.
    • uniform 으로 추가하여 fragment shader에 전달
1
2
uniform vec3 viewPos;
1
2
lightingShader.setVec3("viewPos", camera.Position);
  • 이제 specular를 계산할 수 있다.
    • 먼저 specular 강조 값을 중간으로 정의하여, 하이라이트를 적절히 조절한다.(1.0f이면 아주 밝음)
1
2
float specularStrength = 0.5;
  • 그 다음, view 방향 벡터와 해당 반사 벡터를 계산한다.
    • lightDir 벡터의 부호가 -로 바뀐것 주의
    • reflect 함수
      • 파라미터1: 광원으로부터 fragment 위치로 향하는 벡터
        • lightDir는 그 반대 방향의 벡터이다. (lightDir 벡터 계산할 때 뺠셈 순서 때문)
      • 파라미터2: 정규화된 법선벡터
1
2
3
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
  • 그런 다음, 공식을 사용하여 specular 컴포넌트를 계산해야한다.
    1. 내적: view 방향과 reflect 방향
    2. 음수 방지
    3. 32제곱: 32값은 하이라이트의 shininess 값(낮을수록 빛을 퍼지게, 높을수록 빛을 퍼지게하지않고, 적절히 반사, 하이라이트가 작아짐, 아래 이미지 참고)
1
2
3
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;

  • 마지막으로, ambient와 diffuse 와 함께 계산하는것이다.
1
2
3
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
  • 이제 이 코드를 실행하면 아래와 같이 보일것이다.

초기의 lighting shader들은 vertex shader에서 Phong 모델을 구현했다. 이를 vertex shader에서 구현하는 것은 vertex의 수가 fragment 보다 적기 때문에 계산량이 적어 효율적이었음. 하지만 vertex shader의 최종 컬러 값은 오직 vertex만의 lighting 컬러이므로, fragment에서는 보간된 컬러가 보여진다.

이와 같이 vertex에 구현된 모델을 Gouraud shading이라고 부름.

  • 셰이더는 이처럼 강력하다.
    • 약간의 정보와 함께 shader는 모든 오브젝트에 대해 영향을 끼친다.

출처

learnopengl-basic lighting

This post is licensed under CC BY 4.0 by the author.

[백준][C++] 11066: 파일 합치기 (dp)

[learn-opengl] Lighting: Materials