본문 바로가기

Game/Graphics

Learn OpenGL - Advanced Lighting : Point Shadows (1)

link : https://learnopengl.com/Advanced-Lighting/Shadows/Point-Shadows


Point Shadows


 마지막 튜토리얼에서는 그림자 맵핑을 사용해 동적 그림자를 만드는 방법을 배웠다. 이것은 훌륭하게 작동하지만 그림자가


광원의 한 방향으로만 생성되기 때문에 방향성 조명에만 적합하다. 따라서 방향 맵은 빛이 보고 있는 방향에서 생성되므로


방향 쉐도우 맵핑으로도 알려져 있다.



 이 튜토리얼에서 초점을 맞출 것은 주변의 모든 방향에서 동적인 그림자를 생성하는 것이다. 우리가 사용하고 있는 기술은


실제 점 광원이 모든 방향으로 그림자를 드리우기 때문에 점 광원에 완벽하다. 이 기술은 point shadows(light) 또는 이전에는


omnidirectional shadow 맵으로 알려져 있다.

이 튜토리얼은 이전의 그림자 맵핑 튜토리얼을 기반으로 하므로 기존의 쉐도우 맵핑에 익숙하지 않으면 쉐도우 맵핑 튜토리얼을 먼저 읽는 것이 좋다.

 알고리즘은 방향 그림자 맵핑과 거의 동일하게 유지된다. 광원의 관점에서 깊이 맵을 생성하고 현재 조각 위치를 기반으로 깊이 맵을


샘플링하고, 각 조각을 저장된 깊이 값과 비교해 그림자에 있는지 확인한다. 지향성 그림자 맵핑과 무 지향성 쉐도우 맵핑의


주요 차이점은 사용된 깊이 맵이다.



 우리가 필요로 하는 깊이 맵은 점 광원의 모든 주변 방향에서 장면을 렌더링해야하며 정상적인 2D 깊이 맵은 작동하지 않는다.


대신에 큐브 맵을 사용한다면 어떨까? 큐브 맵은 단지 6개의 면만 있는 환경 데이터를 저장할 수 있기 때문에 큐브 맵의


각면에 전체 장면을 렌더링하고 이를 포인트 라이트의 주변 깊이 값으로 샘플링하는 것이 가능하다.


Image of how omnidrectional shadow mapping or point shadows work


 생성된 깊이 큐브 맵은 방향 벡터로 큐브 맵을 샘플링해 해당 조각에서 깊이를 얻는 라이팅 조각 쉐이더로 전달된다. 우리가 이미


쉐도우 맵핑 튜토리얼에서 설명한 복잡한 것들이 대부분이다. 이 알고리즘을 약간 더 어렵게 만드는 것은 깊이 큐브 맵 생성이다.



Generating the depth cubemap


 빛의 주변 깊이 값을 큐브 맵으로 만드려면 장면을 각 면에 한 번씩 6번 렌더링해야한다. 이것을 하기 위한 하나의 방법은 매번 6개의


서로 다른 뷰 매트릭스로 장면을 6번 렌더링하고, 매번 다른 큐브 맵면을 프레임 버퍼 오브젝트에 첨부한다. 이것은 다음과 같이 보인다:

for(unsigned int i = 0; i < 6; i++)
{
    GLenum face = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i;
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, face, depthCubemap, 0);
    BindViewMatrix(lightViewMatrices[i]);
    RenderScene();  
}

 이것은 하나의 깊이 맵에 많은 렌더 호출이 필요하기 때문에 상당히 비쌀 수 있다. 이 튜토리얼에서는 기하학적 쉐이더에서 약간의 트릭을


사용하는 대안적인 접근법을 사용한다. 이 기법을 사용하면 단 한 번의 렌더링 패스로 깊이 큐브 맵을 작성할 수 있다.



 먼저 큐브 맵을 만들어야한다:

unsigned int depthCubemap;
glGenTextures(1, &depthCubemap);

 그리고 하나의 큐브 맵면을 2D 깊이 값 텍스처 이미지로 생성한다:

const unsigned int SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
for (unsigned int i = 0; i < 6; ++i)
        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_DEPTH_COMPONENT, 
                     SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);  

 또한, 적합한 텍스처 매개변수를 설정하는 것을 잊지말아라:

glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);  

 일반적으로 우리는 큐브 맵 텍스처의 한 면을 프레임 버퍼 객체에 첨부하고 프레임 버퍼의 깊이 버퍼 대상을 다른 큐브 덤프면으로 전환 할 때마다


장면을 6번 렌더링한다. 우리가 한 번의 패스로 모든 면에 렌더할 수 있는 기하 구조 쉐이더를 사용할 것이기 때문에 glFramebufferTexture를 사용해


큐브 맵을 프레임 버퍼의 깊이 첨부로 직접 첨부 할 수 있다:

glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCubemap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);  

 다시 glDrawBuffer와 glReadBuffer에 대한 호출을 주목해라. 깊이 큐브를 생성할 때 깊이 값만 신경써서 OpenGL에게 명시적으로 말해야하므로


이 프레임 버퍼 객체가 컬러 버퍼에 렌더링되지 않는다.



 무지향성 쉐도우 맵에서는 두 개의 렌더링 패스가 있다. 먼저 깊이 맵을 생성하고 두 번째로는 일반 렌더 패스에서 깊이 맵을 사용해 씬에 그림자를


만든다. 프레임 버퍼 객체와 큐브 맵을 가지고 진행한 이 과정은 다음과 같이 보인다:

// 1. first render to depth cubemap
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 cubemap)
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
RenderScene();

 이 과정은 2D 깊이 텍스처와 달리 큐브 깊이 텍스처를 렌더링하고 사용하지만, 프로세스는 기본 그림자 맵핑과 정확히 동일하다.


우리가 실제로 모든 빛의 보기 방향에서 장면을 렌더링하기 전에 먼저 적절한 변환 행렬을 계산해야한다.



Light space transform


 프레임 버퍼와 큐브 맵을 설정하면 모든 장면의 기하를 빛의 모든 6 방향에서 관련 빛 공간으로 변환 할 수 있는 방법이 필요하다.


그림자 맵핑 튜토리얼과 유사하게 우리는 밝은 공간 변환 행렬 T를 필요로 할 것이다. 그러나 이번에는 각 면에 대해 하나씩 할 것이다.



 각각의 광 공간 변환 행렬은 projection 과 view 매트릭스를 모두 포함한다. 투영 행렬의 경우 투영 행렬을 사용한다.


광원은 공간의 한 지점을 나타내므로 원근 투영이 가장 적합하다. 각 조명 공간 변환 행렬은 동일한 투영 행렬을 사용한다:

float aspect = (float)SHADOW_WIDTH/(float)SHADOW_HEIGHT;
float near = 1.0f;
float far = 25.0f;
glm::mat4 shadowProj = glm::perspective(glm::radians(90.0f), aspect, near, far); 

 glm::perspective의 field of view 매개 변수는 90도까지 설정해야한다. 이 값을 90도로 설정하면 큐브 맵의 한 면을 올바르게 채울 수 있는


보기 영역이 정확히 충분히 넓어 모든 면이 가장자리에서 서로 올바르게 정렬되도록한다.



 투영 행렬은 방향마다 변하지 않으므로 각 변환 행렬에 대해 다시 사용할 수 있다. 우리는 방향마다 다른 view 매트릭스가 필요하다.


glm::lookAt을 사용해 6개의 view 방향을 생성한다. 각 방향은 right, left, top, bottom, near, far 순서로 큐브 맵의 단일 방향을 보고 있다:

std::vector<glm::mat4> shadowTransforms;
shadowTransforms.push_back(shadowProj * 
                 glm::lookAt(lightPos, lightPos + glm::vec3( 1.0, 0.0, 0.0), glm::vec3(0.0,-1.0, 0.0));
shadowTransforms.push_back(shadowProj * 
                 glm::lookAt(lightPos, lightPos + glm::vec3(-1.0, 0.0, 0.0), glm::vec3(0.0,-1.0, 0.0));
shadowTransforms.push_back(shadowProj * 
                 glm::lookAt(lightPos, lightPos + glm::vec3( 0.0, 1.0, 0.0), glm::vec3(0.0, 0.0, 1.0));
shadowTransforms.push_back(shadowProj * 
                 glm::lookAt(lightPos, lightPos + glm::vec3( 0.0,-1.0, 0.0), glm::vec3(0.0, 0.0,-1.0));
shadowTransforms.push_back(shadowProj * 
                 glm::lookAt(lightPos, lightPos + glm::vec3( 0.0, 0.0, 1.0), glm::vec3(0.0,-1.0, 0.0));
shadowTransforms.push_back(shadowProj * 
                 glm::lookAt(lightPos, lightPos + glm::vec3( 0.0, 0.0,-1.0), glm::vec3(0.0,-1.0, 0.0));

 여기에서는 6개의 뷰 행렬을 만들고 이를 projection 행렬에 곱해 총 6개의 다른 빛 공간 변환 행렬을 얻는다.


glm::lookAt의 target 매개 변수는 각각 하나의 큐브 맵 면 방향을 조사한다.



 이러한 변환 매트릭스는 큐브 맵에 깊이를 렌더링하는 쉐이더로 전송된다.



Depth shaders


 깊이 큐브 맵에 깊이 값을 렌더링하려면 총 3개의 쉐이더가 필요하다. 즉, 정점 쉐이더와 조각 쉐이더 및 중간의 기하 쉐이더이다.



 기하 쉐이더는 모든 세계 공간 정점을 6개의 다른 밝은 공간으로 변환하는 책임이 있는 쉐이더이다.


따라서 정점 쉐이더는 단순히 정점을 세계 공간으로 변환하고 이를 기하 쉐이더로 전달한다:

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

uniform mat4 model;

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

 그런 다음 기하 구조 쉐이더는 삼각형 정점과 빛 공간 변환 행렬의 균일한 배열을 입력으로 사용합니다. 그러면 형상 쉐이더는 정점을


정점을 밝은 공간으로 변환하는 역할을 합니다. 이것은 흥미로운 곳이기도 한다.



 기하학 쉐이더에는 gl_Layer 라는 빌트인 변수가 있는데 이 큐브 페이스는 프리미티브를 내보낼 큐브 페이스를 지정한다.


그대로 두면 기하 구조 쉐이더는 평범한 것처럼 파이프 라인 아래로 기본 요소를 보낸다. 그러나 이 변수를 업데이트 할 때


각 프리미티브에 대해 렌더링 할 큐브 맵면을 제어 할 수 있다. 이것은 물론 활성 프레임 버퍼에 큐브 맵 텍스처가 부착 된 경우에만 작동한다.

#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices=18) out;

uniform mat4 shadowMatrices[6];

out vec4 FragPos; // FragPos from GS (output per emitvertex)

void main()
{
    for(int face = 0; face < 6; ++face)
    {
        gl_Layer = face; // built-in variable that specifies to which face we render.
        for(int i = 0; i < 3; ++i) // for each triangle's vertices
        {
            FragPos = gl_in[i].gl_Position;
            gl_Position = shadowMatrices[face] * FragPos;
            EmitVertex();
        }    
        EndPrimitive();
    }
}  

 이 형상 쉐이더는 비교적 간단해야한다. 우리는 삼각형을 입력으로 받아 총 6개의 삼각형을 출력한다. (6개의 꼭지점이 18개의 꼭지점에 해당)


main 함수에서 우리는 얼굴 정수를 gl_Layer에 저장해 각 면을 출력면으로 지정하는 6개의 큐브면을 반복한다.


그런 다음 FragPos에 얼굴의 밝은 공간 변환 행렬을 곱해 각 세계 공간 정점을 관련 빛 공간으로 변환해 각 삼각형을 생성한다.


결과 값인 FragPos 변수를 깊이 값을 계산하는데 필요한 조각 쉐이더에 보냈음에 유의해라.



 마지막 튜토리얼에서는 빈 조각 쉐이더를 사용해 OpenGL에서 깊이 맵의 깊이 값을 계산했다. 이번에는 각 조각 위치와 광원 위치 사이의 직선


거리로 우리 자신의 깊이를 계산할 것입니다. 우리 자신의 깊이 값을 계산하면 나중에 그림자 계산을 좀 더 직관적으로 할 수 있다.

#version 330 core
in vec4 FragPos;

uniform vec3 lightPos;
uniform float far_plane;

void main()
{
    // get distance between fragment and light source
    float lightDistance = length(FragPos.xyz - lightPos);
    
    // map to [0;1] range by dividing by far_plane
    lightDistance = lightDistance / far_plane;
    
    // write this as modified depth
    gl_FragDepth = lightDistance;
}  

 조각 쉐이더는 기하 쉐이더에서 FragPos, 라이트의 위치 벡터 및 절두 원의 평면 값을 입력으로 받는다. 여기에서 조각과 광원 사이의


거리를 가져와서 [0,1] 범위에 맵핑하고 조각의 깊이 값으로 쓴다.



 이 쉐이더와 큐브 맵 첨부 프레임 버퍼 오브젝트를 사용해 장면을 렌더링하면 두 번째 패스의 그림자 계산을 위해 완전히 채워진 깊이 큐브


맵을 제공해야 한다.