ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [53] - Lighting(Illumination) - 2
    Graphics 2021. 9. 15. 18:18

    이전에 이론적으로 Phong 모델이 무엇인지 공부했습니다. 이번에는 D3D을 바탕으로 구현한 Engine을 뜯어보면서 어떤 식으로 구성하면 좋을지 배워봅니다. (hw3d/hw3d at T24.1-End · planetchili/hw3d (github.com)) Chili에게 많은 Insight을 얻을 수 있습니다. 비록 영어이긴 하지만 양질의 정보를 제공해줍니다. ((28) C++ 3D DirectX Tutorial [Dynamic Lighting / Graphics Debugger] Part 1 - YouTube) 3D fundamental 시리즈는 솔직히 Fundamental이라고 볼 수 없는 난이도였습니다. 설명도 부족하고 알아듣기도 어려워서 3D graphics의 기초는 대학 강의시간에 배웠던 것들을 최대한 되살려 보충했습니다.

    이전엔 적지 않았지만 이 빛에 관련된 Shading은 Vertex Shader가 아니라 Pixel Shader 또는 Fragment Shader에서 처리합니다. 왜냐하면 정점[Vertex] 세 개로 이루어진 삼각형이 있다고 해봅시다. Vertex Shader 단계에서는 각 정점에 대한 Normal vector와 위치에 대한 정보는 있습니다. 또한 각 정점의 색[Colour]에 대한 정보까지 얻을 수 있습니다. 문제는 내부에 있는 정보가 아직 가공된 상태가 아니란 것입니다. 이전에 아마 Fragment shader에 대한 공부를 하며 해당 단계에서 내부에 대한 정보를 가공한다는 것을 기록한 적이 있을 것입니다. 따라서 각 Vertex 내부의 Fragment을 가공하는 단계에서 이 문제를 함께 처리하는 것입니다.

    figure 1a

    Phong Pixel Shader이며 뭔가 잔뜩 있습니다. HLSL에서 float3는 삼차원 벡터를 의미합니다. 따라서 vToL은 Vector to Light의 약자로써 표면부터 광원에 이르는 벡터를 구한 것입니다. distance to light인 distToL은 vToL의 길이[크기]를 구한 것이고 벡터를 크기로 나누면 방향을 알 수 있기 때문에 direction to light을 그렇게 구해 놓은 것입니다. attenuation은 직역하면 '감쇠'의 의미를 갖고 있어서 빛이 멀어질 수록 어두워지는 것을 표현한 것입니다.

    이것은 논리적으로 타당합니다. 다음과 같은 그림을 상상해봅시다.

    figure 1b

    완전히 같은 크기의 판넬이라면 광원에 가까울 수록 많은 광자들을 차단할 것이고 멀리 떨어진 판넬보다는 더 밝게 묘사될 것입니다. Photon density[Flux]가 광원에 근접할 수록 증가한다는 표현을 쓸 수도 있다고 합니다. 대학 시절 교양강좌로 들었던 강의를 떠올려보면 항성의 밝기는 거리의 제곱에 반비례한다고 했습니다. 물론 멀리 떨어진 천체가 내는 빛을 집광하면 밝아지기는 하겠으나 그런 예는 무시합니다. 그래서 간단하게 그림으로 나타낸다면 왼쪽과 같을 것입니다. 1rㅇ[서 빛이 L만큼 밝다면 거리가 6r이라면 1/36L이 될 것입니다.

    그런데 빛은 거리의 제곱에 반비례하기 때문에 조금만 멀어져도 엄청나게 감소하는 모습을 보여줍니다. 그래프를 통해서 생각하면 직관적으로 와닿습니다.

    이것이 1/x^2의 그래프입니다. 0에서는 정의가 되지 않기 때문에 무한에 가깝게 상승하는 모습을 볼 수 있으며 x = 3만 되어도 0에 근접하게 되는 것을 알 수 있습니다.

    그래서 D3D에서는 분모에 적절한 변수와 상수를 조합하여 다채로운 빛 효과를 모색합니다. 가령, x+1과 같은 수를 분모에 추가한다면 거리에 따라 감쇠하는 빛을 완화할 수 있을 것입니다.

    figure 1c

    그러면 attenuation의 식이 가지는 의미를 알 수 있어야 합니다. 다시 attenuation의 변수를 살펴보도록 합시다.

    figure 1d

    distToL는 광원으로부터 물체가 떨어진 거리를 구한 것이라고 했습니다. 밝기의 감쇠는 거리의 제곱에 반비례하기 때문에 distToL * distToL는 타당한 설정값입니다. 하지만 위에서 말했던 것처럼 제곱에 반비례는 감쇠의 정도가 너무 급격하기 때문에 완충장치를 요구했습니다. 그 역할을 하는 것이 attConst, attLin, attQuad입니다. 이 상수들을 분모에 적절하게 도입하여 물체의 표면이 광원과의 거리에 따라 부드럽게 때로는 극적으로 변화할 수 있도록 설정해준 장치가 되는 것입니다. (-Point Light Attenuation | Ogre Wiki (ogre3d.org)) 이 사이트에서 이와 관련한 지식을 얻을 수 있습니다.

    또 한 가지 주목해야 할 것은 마지막 반환 값입니다. diffuse+ambient+specular에 물체의 표면 RBG을 곱한 값을 반환합니다. 물체 자체가 내보내는 Emissive는 볼 수 없지만 이론 상으로 공부했던 내용이 그대로 구현되어 있습니다. Diffuse의 값을 구하는 과정을 보면 광원 자체의 RGB diffuseColour에 강도를 곱하고 감쇠 값을 곱합니다. 그리고 normal vector n과 빛을 향한 벡터 Direction to light 벡터의 Dot Product와 0.0 중에서 큰 값을 곱하는 것까지 일치합니다.

    참고로 HLSL에서 두 벡터의 곱을 Asterisk * 으로 표현하면 아다마르[Hadamard Product] 곱을 의미합니다. 선형대수 시간에 배웠던 Dot Product는 dot(v,u) 함수를 호출해 사용합니다. 이 HLSL 함수들은 특히 Intrinsic Functions(Intrinsic Functions - Win32 apps | Microsoft Docs)에 잔뜩 있습니다. 다 외우기는 벅차고 자주 쓰는 것들만 알아두면 좋을 것 같습니다. 그래서 마지막 반환 값의 Saturate는 argument을 [0,1) 사이에 Clamp하는 역할을 한다고 합니다. 

    figure 1e

    Phong의 Vertex Shader입니다. Vertex의 위치와 Normal vector에 대한 정보를 받습니다. model은 object space 상의 물체의 위치이며 여기에 modelView로 선언한 World Transform을 위하면 물체[Mesh, Obejct]의 World space 상의 위치인 World position을 얻습니다. noramld은 object space상의 normal vector n에 world transform을 곱하며 전에도 공부했던 것처럼 normal vector는 non-uniform scaling의 경우 기대하는 값으로 변환이 되지 않기 때문에 애초부터 변환행렬의 Inverse Transpose matrix을 사용해야 한다는 사실을 잊으면 안됩니다. ([13] - Vertex processing - 1 :: 소오설 (tistory.com)) 계산된 행렬을 반환하기까지 어려운 건 없습니다.

    Point Light and Graphics debug

    현 튜토리얼에선 광원을 하나로 두고 스크린 상에 있는 모든 물체에 적용되도록 만들 예정이었습니다. 그리고 광원은 어떤 물체가 아니라 논리에 가까운 개념이었기 때문에 실체가 없었습니다. 그렇기 때문에 처음 빛 효과를 주기 위한 설계단계에서는 각 물체에 PhongLightConstants라는 구조체에 Pixel Shader에 넣어줄 Constant Buffer을 각각 생성했습니다. 하지만 이런 식으로 설계하게 되면 광원을 어떤 글로벌 오브젝트로 구현했을 때, 광원의 움직임에 따라 그림자가 생기거나 빛의 강도가 바뀌는 현상을 구현할 수가 없게 됩니다. 왜냐하면 각각의 오브젝트마다 고정된 위치에 광원이 있다고 여겨지기 때문입니다. 따라서 광원에 대한 상수 버퍼[Constant Buffer]는 각각의 오브젝트에 있어서는 안됩니다.

    figure 2a

    위와 같이 Box.cpp에서 pixel shader에 constant buffer을 넘겨주면 광원을 아무리 움직여도 고정된 위치에 광원이 있는 것 처럼 변화를 볼 수 없습니다.

    figure 2b

    위와 같은 현상을 해결하기 위해서 Visual Studio가 제공하는 Graphics Debug을 열어서 특정 Frame을 분석하면 다음과 같은 결과를 확인할 수 있습니다.

    figure 2c

    위와 같이 한 픽셀을 찍어서 어떤 일이 일어나고 있는지를 분석할 수 있습니다. 빨간 십자선의 상자에서 나타나는 일을 분석해보면 다음과 같습니다.

    figure 2d

    이렇게 Pixel History에서 Figure 2c에서 찍은 상자의 삼각형이 현재 명령까지 이르기까지 로그를 전부 볼 수 있습니다. 여기를 보면 이 오브젝트를 그리라고 명령한 번호가 빨간색으로 721번이라고 적혀있습니다. 그러면 보통 화면 좌측에 위치하는 Event List tab에서 다음 명령어 번호를 찾습니다.

    figure 2e

    해당 탭을 열어보면 어떤 figure 2g와 같이 이 오브젝트가 생성 되기까지 어떤 함수들을 거쳤는지 모두 볼 수 있습니다. 그러면 Event Call Stack에 이런 것들이 보입니다. Graphics에서 Drawindexed을 호출한 것은 당연한 것입니다. 그런데 이걸 더블 클릭하면 엉뚱한 곳으로 연결됩니다. 기대하길, 독립적인 광원인 Point Light로 이어져야 하는데 figure 2a의 Box.cpp로 이어집니다. 그래서 Pixel shader을 위한 constant buffer을 bind하는 곳을 빼면 아마 될 것으로 예상이 되지만 그래도 변화는 확인할 수 없습니다. Box.cpp에서 bind하던 constant buffer을 PointLight에서 bind해야 합니다.

    figure 2f

    Visual Studio의 Graphics debugger는 유용할 수 있겠지만 사용이 너무 어렵습니다. 영어로 된 문서뿐이고 환경에 따라서 될 수도 있고 안 될 수도 있고 왜 안 되는지 알 방법도 거의 없어서 미쳐버리는 줄 알았습니다. figure 2f의 Event Call Stack도 원래는 PS constant buffer 을 클릭하면 해당 buffer에 대한 자세한 내용을 알 수 있어야 하는데 이유는 모르겠지만 call stack을 불러오지 못합니다. 해결 중에 있긴 한데 

    figure 2g

     

    'Graphics' 카테고리의 다른 글

    [52] - Lighting(Illumination)  (0) 2021.09.13
    [51] - imgui  (0) 2021.08.28
    [50] - Texture - 2  (0) 2021.08.26
    [49] - Texture - 1  (0) 2021.08.17
    [48] - Rasterization  (0) 2021.08.13
Designed by Tistory.