본문 바로가기

Game/Graphics

Learn OpenGL - Advanced Lighting : Shadow Mapping (1)

link : https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping


Shadow Mapping


 그림자는 폐색으로 인한 빛의 부재로 인한 결과이다. 광원의 광선이 어떤 물체에 의해 가려지기 때문에 물체에 부딪치지 않으면


그 물체는 그림자 속에 있다. 그림자는 밝은 장면에 많은 사실감을 더하고, 관찰자가 물체 간의 공간적 관계를 더 쉽게 관찰


할 수 있게 한다. 그들은 우리 장면과 사물에 깊이 감을 준다. 예를 들어, 그림자가 있거나 없는 장면의 다음 이미지를 살펴보자:


comparrison of shadows in a scene with and without in OpenGL


 그림자를 사용하면 오브젝트가 서로 어떻게 관련되는지 훨씬 더 분명해진다는 것을 알 수 있다. 예를 들어, 큐브 중 하나가 다른 큐브 위로


떠 다니는 사실은 그림자가 있을 때 훨씬 더 두드러진다.



 현재의 실시간 연구에서 완벽한 그림자 알고리즘이 아직 개발되지 않았기 때문에 그림자는 구현하기가 다소 까다롭다. 몇 가지 좋은


음영 근사법이 있지만, 그것들은 모두 우리가 고려해야만 하는 약간의 단점과 성가심을 가지고 있다.



 대부분의 비디오 게임에서 사용되는 한 가지 기술은 적절한 결과를 제공하고 비교적 쉽게 구현할 수 있는 쉐도우 맵핑이다.


그림자 맵핑은 너무 이해하기 어렵지 않고 성능도 많이 들지 않으며, 전 방향성 음영 맵과 계단식 음영 맵과 같은 고급 알고리즘으로


쉽게 확장된다.




Shadow mapping


 그림자 매핑 뒤에 있는 개념은 매우 간단하다. 빛의 관점에서 장면을 렌더링하고 빛의 관점에서 볼 때 모든 것이 켜져 있고


볼 수 없는 모든 것이 그림자에 있어야한다. 자체와 광원 사이에 큰 상자가 있는 바닥 섹션을 상상해보아라.


Shadow mapping illustrated.


 여기서 파란색 선은 광원이 볼 수 있는 조각을 나타낸다. 그림자가 있는 부분으로 렌더링된다. 광원으로부터 선 또는 광선을 가장 오른쪽


상자의 단편에 그릴 경우 가장 오른쪽에 있는 컨테이너를 치기 전에 먼저 광선이 떠다니는 컨테이너를 때리는 것을 볼 수 있다.


결과적으로, 부동 컨테이너의 조각이 켜지고 맨 오른쪽 컨테이너의 조각이 켜지지 않으므로 그림자가 생긴다.



 우리는 광선에 처음으로 물체를 치는 지점을 얻고 이 가장 가까운 지점을 이 광선의 다른 지점과 비교하려고 한다. 그런 다음 기본 테스트를


수행해 테스트 포인트의 광선 위치가 가장 가까운 포인트보다 광선 아래에 있는지 확인한다. 그렇다면 테스트 포인트가 그림자에 있어야한다.


그러한 광원으로부터의 수천 개의 광선을 반복해서 반복하는 것은 극히 비효율적인 접근이며, 실시간 렌더링에 좋지 않다.


대신 우리는 익숙한 것을 사용할 것이다: the depth buffer.



 깊이 테스트 튜토리얼에서 깊이 버퍼의 값은 카메라의 관점에서 [0,1]로 고정된 조각의 깊이에 해당한다는 것을 기억할 것이다.


빛의 관점에서 장면을 렌더링하고 결과 깊이 값을 텍스처에 저장한다면 어떨까? 이렇게하면 광원의 관점에서 볼 때 가장 가까운


깊이 값을 샘플링 할 수 있다. 결국 깊이 값은 광원의 관점에서 볼 수 있는 첫 번째 조각을 보여준다. 이 모든 깊이 값을 깊이 맵


또는 그림자 맵이라고하는 텍스처에 저장한다.


Different coordinate transforms / spaces for shadow mapping.


 왼쪽 이미지는 큐브 아래의 표면에 그림자를 드리우는 방향 광원을 보여준다. 깊이 맵에 저장된 깊이 값을 사용해 가장 가까운 점을


찾고 이를 사용해 조각이 그림자에 있는지 여부를 확인한다. 우리는 광원에 특정한 뷰 및 투영 행렬을 사용해 장면을 렌더링함으로써


깊이 맵을 생성한다. 이 투영 및 뷰 매트릭스는 3D 위치를 조명의 가시적인 좌표 공간으로 변환하는 변환 T를 함께 형성한다.

Directional light는 무한히 멀리 모델링된 위치를 갖지 않는다. 그러나 그림자 매핑을 위해 우리는 빛의 관점에서 장면을 렌더링해야하므로 빛 방향의 어딘가의 위치에서 장면을 렌더링해야한다.

 오른쪽 이미지에서 우리는 동일한 지향성 빛과 뷰어를 본다. 우리는 그림자가 있는지 여부를 결정해야하는 지점 P에서 조각을 렌더링한다.


이렇게 하기 위해 우리는 먼저 T를 사용해 점 P를 빛의 좌표 공간으로 변환한다. 점 P는 빛의 관점에서 본 것처럼 이제 Z 좌표는 이 깊이의


깊이에 해당한다. (이 예에서는 0.9) 점 P를 사용해 깊이 맵을 인덱싱해 광원의 관점에서 가장 가까운 눈에 보이는 깊이를 얻을 수 있다.


이 깊이는 샘플 깊이 0.4의 지점 C에 있다. 깊이 맵을 인덱싱하면 점 P에서의 깊이보다 더 작은 깊이가 반환되었으므로 점 P가 폐색되어


그림자에 있다고 결론을 내릴 수 있다.



 그림자 매핑은 두 가지 패스로 구성된다. 먼저 깊이 맵을 렌더링하고 두 번째 패스에서 장면을 보통으로 렌더링하고 생성된 깊이 맵을


사용해 조각이 그림자에 있는지 여부를 계산한다. 조금 복잡해 보일지 모르지만 기술을 단계별로 실행하자마자 의미가 있을 것이다.




The depth map


 첫 번째 단계에서는 깊이 맵을 생성해야한다. 깊이 맵은 그림자를 계산할 때 사용하는 광원의 관점에서 렌더링된 깊이 텍스처이다.


장면의 렌더링된 결과를 텍스처에 저장해야 하기 때문에 프레임 버퍼가 다시 필요하다.



 먼저 깊이 맵을 렌더링하기 위한 프레임 버퍼 객체를 만든다:

unsigned int depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);  

 다음으로 우리는 프레임 버퍼의 깊이 버퍼로 사용할 2D 텍스처를 만든다:

const unsigned int SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;

unsigned int depthMap;
glGenTextures(1, &depthMap);
glBindTexture(GL_TEXTURE_2D, depthMap);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 
             SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);  

 깊이 맵을 생성하는 것은 너무 복잡해 보이지 않아야 한다. 우리는 깊이 값만 염려하기 때문에 텍스처의 형식을 GL_DEPTH_COMPONENT로


지정한다. 텍스처의 너비와 높이를 1024로 지정한다. 이것은 깊이 맵의 해상도이다.



 생성된 깊이 텍스처를 사용해 프레임 버퍼의 깊이 버퍼로 연결할 수 있다:

glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);  

 광원의 관점에서 장면을 렌더링 할 때 깊이 정보가 필요하기 때문에 색상 버퍼가 필요하지 않다. 그러나 framebuffer 객체는 색상 버퍼가


없으면 완전하지 않으므로 OpenGL에 명시적으로 말해 색상 데이터를 렌더링하지 않아도 된다. glDrawBuffer와 glReadBuffer를 사용해


읽기와 그리기 버퍼를 모두 GL_NONE으로 설정하면된다.



 텍스처에 깊이 값을 렌더링하는 제대로 구성된 프레임 버퍼를 사용하면 첫 번째 패스를 시작할 수 있다: generating the depth map


두 패스의 전체 렌더링 단계는 다음과 같이 보인다:

// 1. first render to depth map
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
    glClear(GL_DEPTH_BUFFER_BIT);
    ConfigureShaderAndMatrices();
    RenderScene();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 2. then render scene as normal with shadow mapping (using depth map)
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
glBindTexture(GL_TEXTURE_2D, depthMap);
RenderScene();

 이 코드는 세부 사항을 생략했지만 쉐도우 맵핑에 대한 일반적인 아이디어를 제공한다. 여기서 중요한 점은 glViewport에 대한 호출이다.


그림자 맵은 원래 장면을 렌더링하는 것과 다른 해상도를 가지고 있기 때문에 그림자 맵의 크기를 수용하기 위해 뷰포트 매개 변수를


변경해야한다. 뷰포트 매개 변수를 업데이트하는 것을 잊어버리면 결과 깊이 맵이 불완전하거나 작아진다.



Light space transform


 이전 스니펫 코드에서 알 수 없는 것은 ConfigureShaderAndMatrices 함수이다. 두 번째 단계에서 이것은 평소와 같이 비즈니스이다.


적절한 투영 및 뷰 행렬이 설정되고 객체당 관련 모델 행렬이 있는지 확인해라. 그러나 첫 번째 패스에서 우리는 빛의 관점에서 장면을


렌더링하기 위해 다른 투영 및 뷰 매트릭스를 사용한다.



 방향성 광원을 모델링하기 때문에 모든 광선이 평행하다. 이러한 이유 때문에 원근감 변형이 없는 광원에 직교 투영 행렬을 사용할 것이다:

float near_plane = 1.0f, far_plane = 7.5f;
glm::mat4 lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);  

 다음은 이 튜토리얼의 데모 씬에서 사용된 직교 투영 행렬의 예이다. 투용 행렬은 가시적인 것의 범위를 간접적으로 결정하기 때문에


클리핑되지 않은 것은 투영 절두체의 크기가 깊이 맵에 있어야 할 객체를 정확하게 포함하는지 확인하기를 원할 것이다.


오브젝트나 조각이 깊이 맵에 없으면 그림자가 생기지않는다.



 빛의 관점에서 볼 수 있도록 각 객체를 변형하기 위해 뷰 매트릭스를 생성하려면 악명 높은 glm::lookAt 함수를 사용해라.


이번에는 광원의 위치가 장면의 중심을 바라본다:

glm::mat4 lightView = glm::lookAt(glm::vec3(-2.0f, 4.0f, -1.0f), 
                                  glm::vec3( 0.0f, 0.0f,  0.0f), 
                                  glm::vec3( 0.0f, 1.0f,  0.0f));  

 이 두 가지를 결합하면 광원에서 볼 수 있는 공간으로 각각의 세계 공간 벡터를 변환하는 밝은 공간 변환 행렬을 얻을 수 있다.


정확하게 우리가 깊이 맵을 렌더링하는데 필요한 것이다:

glm::mat4 lightSpaceMatrix = lightProjection * lightView; 

 이 lightSpaceMatrix는 이전에 T로 표시한 변형 행렬이다. 이 lightSpaceMatrix를 사용하면 쉐이더에 투영 및 뷰 행렬의 등가물을 제공하는


한 장면을 평소와 같이 렌더링 할 수 있다. 그러나 우리는 깊이 값만 신경쓰고 우리의 메인 쉐이더에서는 값 비싼 조각 계산이 전부가 아니다.


퍼포먼스를 저장하기 위해 우리는 깊이 맵에 렌더링하기 위해 다른, 그러나 훨씬 더 간단한 쉐이더를 사용할 것이다.



Render to depth map


 빛의 관점에서 장면을 렌더링 할 때 정점을 밝은 공간으로 변환하는 단순한 쉐이더를 사용하는 것이 훨씬 더 많다.


simpleDepthShader와 같은 단순한 쉐이더의 경우 다음 정점 쉐이더를 사용한다:

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

uniform mat4 lightSpaceMatrix;
uniform mat4 model;

void main()
{
    gl_Position = lightSpaceMatrix * model * vec4(aPos, 1.0);
}  

 이 정점 쉐이더는 lightSpaceMatrix를 사용해 오브젝트별 모델, 정점을 취하고 모든 정점을 밝은 공간으로 변환한다.


컬러 버퍼가 없으므로 결과 조각은 처리가 필요하지 않으므로 빈 조각 쉐이더를 사용해도 된다:

#version 330 core

void main()
{             
    // gl_FragDepth = gl_FragCoord.z;
}  

 빈 조각 쉐이더는 아무런 처리도 하지 않고 실행이 끝나면 깊이 버퍼가 업데이트된다. 우리는 명시적으로 한 줄의 주석 처리를 제거해


깊이를 설정할 수 있다. 그러나 이것은 실제로 장면 뒤에서 발생하게 된다.



 깊이 버퍼 렌더링은 이제 효과적으로 된다:

simpleDepthShader.use();
glUniformMatrix4fv(lightSpaceMatrixLocation, 1, GL_FALSE, glm::value_ptr(lightSpaceMatrix));

glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
    glClear(GL_DEPTH_BUFFER_BIT);
    RenderScene(simpleDepthShader);
glBindFramebuffer(GL_FRAMEBUFFER, 0);  

 여기서 RenderScene 함수는 쉐이더 프로그램을 사용하고 모든 관련 드로잉 함수를 호출하며 필요한 경우 해당 모델 행렬을 설정한다.



 결과는 빛의 관점에서 볼 수 있는 각 조각의 가장 가까운 깊이를 유지하는 멋지게 채워진 깊이 버퍼이다. 이 텍스처를 화면을 채우는


2D 쿼드 위에 투사함으로써 다음과 같은 결과를 얻는다:


Depth (or shadow) map of shadow mapping technique


 깊이 맵을 쿼드에 렌더링하기 위해 우리는 다음과 같은 조각 쉐이더를 사용했다:

#version 330 core
out vec4 FragColor;
  
in vec2 TexCoords;

uniform sampler2D depthMap;

void main()
{             
    float depthValue = texture(depthMap, TexCoords).r;
    FragColor = vec4(vec3(depthValue), 1.0);
}  

 원근 투영을 사용할 때 깊이가 비선형이기 때문에 직교 투영 행렬 대신 원근 투영 행렬을 사용해 깊이를 표시할 때 약간의 변화가 있음에


유의해라. 이 튜토리얼의 마지막 부분에서는 이러한 미묘한 차이점에 대해 설명한다.






** ** ** ** ** ** ** ** ** **


와.. 디버깅하다가 시간 너무 날렸다. 빡친다. 하지만 해결했으니 우선 자자!!