그래픽스에서 조명은 빛과 물체 간의 상호작용을 처리하는 기술이다. 보통 지역 조명과 전역 조명 모델 두 가지로 구분된다.

지역 조명은 광원으로부터 물체에 직접 들어오는 빛만을 고려하기 때문에 표면의 재질과 광원의 속성만으로 표면의 색상이 결정된다.

하지만 실세계의 모든 물체는 간접 조명을 받기 때문에 전역 조명 모델은 모든 물체를 잠재적인 광원으로 취급한다.

그렇다고 전역 조명 모델을 그대로 적용하기에는 너무 많은 연산을 필요로 하기 때문에 실시간에 구현하는 것은 어렵다.

 

그래서 게임에서는 간략화된 전역 조명 알고리즘을 사용하거나 미리 전역 조명을 계산하여 저장한 뒤 런타임에 이를 사용하는 것이 보편적이다.

 

 

퐁 조명 모델

퐁 모델은 지역 조명 모델이다.

퐁 모델의 경우 물체의 표면에서 감지되는 색상은 아래와 같은 4가지 항목에 의해 결정된다.

 

 

◾ 디퓨즈(diffuse) 반사

램버트의 법칙에 따라 설명된다. (*램버트의 법칙 : 물체의 표면에 들어오는 빛의 세기는 표면에서 빛까지의 벡터 l과 법선 n이 이루는 코사인 값에 비례한다. 각도가 작을수록 더 많은 빛을 받는다.)

램버시안 표면 또는 이상적인 디퓨즈 표면으로 들어온 빛은 모든 방향을 따라 같은 강도로 반사된다. (난반사)

단, l과 n이 이루는 각이 둔각인 경우 음수가 나오기 때문에 최소값을 0으로 처리해야 한다.

 

◾ 스페큘러(specular) 반사

물체 표면에 하이라이트를 만드는데 사용된다. 시선 벡터(v)와 반사 벡터(r)가 필요하다.

시선 벡터는 물체 표면의 점 p와 카메라를 연결하고 카메라 시선과 반대 방향으로 정의한다.

반사 벡터는 점 p의 법선 벡터 n과 조명간의 입사각과 동일한 각도를 이루며 반사된다.

 

하이라이트를 볼 수 있는 영역은 r을 중심으로 한 원뿔 모양으로 묘사할 수 있고 v가 원뿔 범위 안에 속한다면 하이라이트를 볼 수 있다.

 

◾ 앰비언트(ambient) 반사

다양한 물체로부터 반사된 빛, 즉 간접 조명을 앰비언트 라이트라고 부른다.

씬의 모든 지점에서 반사된 빛이 어우러진 것이기 때문에 특정 방향이 아닌 모든 방향을 따라서 표면의 p점에 들어오며 마찬가지로 반사된다.

그렇기 때문에 p점에 들어오는 앰비언트 라이트의 양은 p점의 법선에 독립적이고 반사되는 빛은 카메라 시선에 독립적이다.

 

◾ 발산광(emissive light)

물체 스스로 발산하는 빛이다.

지역 조명 모델은 발산광을 가진 물체를 광원으로 취급하지 않기 때문에 같은 공간의 다른 물체의 조명 연산에 영향을 주지 않는다.

 

 

퐁 조명 모델은 위의 4개 항을 더해서 정의된다.

 

 

쉐이더와 쉐이딩 언어

쉐이더 프로그래밍을 위해 선택되는 하이레벨 언어 중 하나는 HLSL, Cg, GLSL가 널리 사용되고 모두 C와 비슷한 언어이다. 이 중 HLSL은 Direct3D에서 사용된다. 책에서는 HLSL에 대해서만 다루게 된다.

 

쉐이더에서 함수는 최상위 함수와 내장 함수로 구분되고 정점 및 프래그먼트 쉐이더 자신은 최상위 함수이다. 최상위 함수는 여러 내장 함수를 호출할 수 있는데 빈번하게 호출하는 함수는 곱셈 연산을 위한 mul이다.

쉐이더는 실행시마다 변하는 데이터와 모든 쉐이더 실행 시 공유되는 데이터 두 가지 형태의 입력 데이터를 받는다.

 

조명은 정점별로 적용할수도 있고 프래그먼트별로 적용할 수도 있다. 대체로 프래그먼트별로 적용하는 것이 더 우수한 결과를 생성한다. 정점별로 적용하면 카메라의 위치에 따라 반사광이 균일하지 않을 수 있지만 프래그먼트별로 적용하면 항상 균일한 반사광을 볼 수 있다.

다만 프래그먼트별로 적용하는 조명에서는 정점 쉐이더의 역할은 미미해진다.

 

 

전역 조명

대표적인 전역 조명 알고리즘으로는 레이 트레이싱과 래디오시티가 있다. 

 

◾ 레이 트레이싱

뷰 프러스텀은 카메라에 수렴하는 투영선의 집합이고 투영선의 개수는 뷰포트의 해상도와 동일하다. 즉, 투영선 하나가 픽셀 하나의 색상을 결정하는 것이다.

레이 트레이싱 알고리즘에서는 뷰 프러스텀이 정의된 카메라 공간 물체에 투영 변환을 적용하는 대신에 각 투영선의 반대 방향으로 광선을 발사하고 이를 추적하여 해당 투영선을 따라서 들어오는 색상을 계산한다.

이게 바로 픽셀 색상이 되고 1차 광선이라 정의할 수 있다.

 

1차 광선이 씬의 어떤 물체와도 부딪히지 않으면 씬의 배경색으로 픽셀 색상이 결정된다. (아무런 물체도 통과하지 못했으니 애초에 물체가 존재하지 않는 영역이라는 뜻 같음)

만약 1차 광선이 어떤 물체와 부딪히면 해당 점이 그림자 안에 있는지를 검사하기 위해 그림자 광선이라는 2차 광선을 각 광원을 향해 발사한다. 이 때 그림자 광선이 광원으로 진행하는 도중에 다른 물체와 부딪히면 해당 교차점은 광원의 직접적인 영향권에 있지 않다는 의미가 된다. 그림자 광선이 광원에 도달하면 교차점에 입사하는 빛을 이용해서 직접 조명 색상을 결정하게 된다.

 

그림자 광선뿐만 아니라 반사 광선과 굴절 광선 2종류가 교차점에서 추가로 발사된다. 이 두가지 광선을 간접 조명을 계산하기 위한 목적으로 발사된다.

 

레이 트레이싱 알고리즘은 재귀적인 알고리즘이고 광선 트리가 씬을 벗어나거나 미리 정의된 재귀 단계에 도달할 때까지 확장된다.

 

 

◾ 래디오시티

디퓨즈 표면 사이에서 반사되는 빛을 시뮬레이션 한다.

래디오시티 알고리즘은 실시간에 구현하기에는 연산량이 너무 많기 때문에 보통 라이트맵으로 미리 만들어둔다.

'이론 > 그래픽스' 카테고리의 다른 글

[Chapter 4] 프래그먼트 처리와 출력 병합  (0) 2023.05.11
[Chapter 3] 래스터라이저  (0) 2023.05.11
[Chapter 2] 정점 처리  (0) 2023.04.27
[Chapter 1] 폴리곤 메쉬  (0) 2022.09.16

엔진과 클라이언트 프로젝트를 분리하고 엔진에서 라이브러리를 뽑아서 사용할 때, 업데이트를 배치파일로 만들었다 하더라도 매번 실행하는 번거로움이 발생했다.

 

그래서 엔진 프로젝트 빌드 후 이벤트로 배치파일 자동 실행으로 해결.

 

CreateVertexBuffer(/*정점 개수 * sizeof(정점타입)*/, 0, /*FVF*/, D3DPOOL_MANAGED, /*정점 버퍼의 주소*/, nullptr);
CreateIndexBuffer(/*인덱스 개수 * sizeof(WORD)*/, D3DUSAGE_WRITEONLY, D3DFMT_INDEX16, D3DPOOL_MANAGED, /*인덱스 버퍼의 주소*/, nullptr);

// D3DFVF_CUSTOMVERTEX는 (D3DFVF_XYZ|D3DFVF_DIFFUSE)를 매크로화 한 것
m_pDevice->CreateVertexBuffer(8 * sizeof(CUSTOMVERTEX), 0, D3DFVF_CUSTOMVERTEX, D3DPOOL_MANAGED, &m_vb, nullptr);
m_pDevice->CreateIndexBuffer(36 * sizeof(WORD), D3DUSAGE_WRITEONLY, D3DFMT_INDEX16, D3DPOOL_MANAGED, &m_ib, nullptr);

인덱스의 순서는 왼손 좌표계를 사용하기 때문에 CW(시계방향)으로 설정해야 한다.

 

 

VOID* pVerts = nullptr;
m_vb->Lock(0, sizeof(verts), (void**)&pVerts, 0);
memcpy(pVerts, verts, sizeof(verts));
m_vb->Unlock();

VOID* pIndices;
m_ib->Lock(0, sizeof(indices), (void**)&pIndices, 0);
memcpy(pIndices, indices, sizeof(indices));
m_ib->Unlock();

이후 버퍼들을 복사한다. Lock과 Unlock은 반드시 한 쌍을 이루어서 사용해야 한다.

 

 

// 뷰 행렬을 구하기 위한 외부 파라미터
D3DXVECTOR3 eye(0.f, 0.f, -5.f);
D3DXVECTOR3 target(0.f, 0.f, 1.f);
D3DXVECTOR3 up(0.f, 1.f, 0.f);

D3DXMATRIX view;
D3DXMATRIX proj;

// 왼손 좌표계를 기준으로 뷰 행렬을 만든다
D3DXMatrixLookAtLH(&view, &eye, &target, &up);
m_pDevice->SetTransform(D3DTS_VIEW, &view);

// 왼손 좌표계를 기준으로 투영 행렬을 만든다
D3DXMatrixPerspectiveFovLH(&proj, D3DX_PI * 0.25f, static_cast<float>(g_iWindowWidth) / g_iWindowHeight, 1.f, 1000.f);
m_pDevice->SetTransform(D3DTS_PROJECTION, &proj);

m_pDevice->SetRenderState(D3DRS_LIGHTING, false);
m_pDevice->SetRenderState(D3DRS_SHADEMODE, D3DSHADE_GOURAUD);

뷰 행렬과 투영 행렬을 구한다.

이대로 출력하면 사각형만 출력되기 때문에 입체감을 전혀 느낄수 없다.

 

 

D3DXMATRIX world;
D3DXMATRIX rx, ry, rz;
D3DXMATRIX scale;
D3DXMATRIX translation;
m_fRotX += 0.001f;
m_fRotY += 0.001f;
m_fRotZ += 0.001f;

D3DXMatrixScaling(&scale, 1.f, 1.f, 1.f);
D3DXMatrixRotationX(&rx, m_fRotX);
D3DXMatrixRotationY(&ry, m_fRotY);
D3DXMatrixRotationZ(&rz, m_fRotZ);
D3DXMatrixTranslation(&translation, 0.f, 0.f, 0.f);

world = scale * rx * ry * rz * translation;

m_pDevice->SetTransform(D3DTS_WORLD, &world);

모든 축으로 회전시키면 잘 회전되며 입체감을 느낄 수 있다.

 

 

인덱스 버퍼를 사용하지 않고도 출력이 가능하지만 인덱스 버퍼를 사용하는 것이 중복된 정점을 만들지 않기 때문에 메모리를 좀 더 절약할 수 있다.

 

정점, 인덱스 설정 -> 정점 버퍼, 인덱스 버퍼 생성 -> 복사 -> 디바이스 설정 -> 렌더 의 과정을 밟는다.

래스터라이저에 의해 생성된 프래그먼트들은 다양한 속성들을 포함할 수 있고 이 속성들을 이용해서 각 프래그먼트의 최종 색상을 결정할 수 있다. 프래그먼트 프로그램은 스크린 영상의 품질에 결정적인 영향을 미치기 때문에 다양한 종류의 알고리즘이 개발되었는데, 주로 조명과 텍스쳐링에 집중된다.

 

 

◾ 텍스쳐링

텍스쳐는 대게 텍셀의 2차원 배열 형태를 가진다 (texture element의 준말. picture element의 준말인 픽셀과 구분하기 위해 사용)

정규화된 텍스쳐 좌표인 uv 좌표를 사용한다.

 

텍스쳐 좌표를 정점에 할당하는 작업을 표면 파라미터화라고 부른다. 이 작업을 위해서는 3차원 메쉬를 2차원 평면에 펼쳐야 한다. 보통 2차원 평면으로 펼치는 과정에서 대부분 왜곡 현상이 일어나지만 왜곡을 최소화 하는 좋은 알고리즘들이 존재한다.

그나마 파라미터화 에러를 줄일 수 있는 방법 중 하나는 메쉬를 여러개로 쪼개서 부분별로 수행하는 것이다.

 

스캔 변환이 완료되면 각각의 프래그먼트는 보간된 uv좌표를 가지게 된다. 이를 텍셀 주소로 매핑하는 작업은 렌더링 과정중에 자동으로 수행된다.

대부분의 경우 텍셀 주소는 부동소수점으로 표현되고 텍셀 주소가 계산되면 주변의 텍셀들을 모아서 최종적인 색상 값을 결정하는 필터링 과정이 필요하게 된다.

 

 

◾ 출력 병합

렌더링 파이프라인의 마지막 단계이다. 프래그먼트의 출력은 단순히 RGB값만 가지고 있는것이 아니라 알파값과 깊이값을 가지고 있기 때문에 RGBAZ 프래그먼트라고도 표현된다. 알파 및 깊이 값을 사용하여 프래그먼트는 컬러 버퍼의 픽셀과 경쟁하기도 하고 결합되기도 한다.

 

동일한 x,y 좌표에 위치한 픽셀의 출력 우선순위는 카메라에 더 가까운쪽이 그려지는것이 이치에 맞는다.

이러한 결정을 하는 알고리즘을 z-버퍼링(깊이 버퍼링)이라고 부른다.

깊이 버퍼는 컬러 버퍼와 동일한 해상도를 가지며 현재 컬러 버퍼에 저장되어 있는 z값을 저장한다. 이후 출력 병합 단계에서 둘을 비교하여 더 작은 z값으로 컬러 버퍼의 픽셀을 대체해가며 픽셀을 결정한다.

 

하지만 반투명 물체를 렌더링할 때에는 얘기가 조금 달라진다. 반투명한 프래그먼트의 z값이 컬러 버퍼에 있는 픽셀의 z값보다 작은 경우 기존대로라면 완전히 대체가 되지만 이때는 픽셀이 프래그먼트를 통해 비쳐서 보여야 한다.

픽셀과 프래그먼트 색상의 혼합이 일어나야 하는 것이고 이를 알파 블렌딩이라 부른다.

알파 채널은 보통 RGB 각각의 채널과 같은 크기의 비트 수를 가진다.

 

중요한 사실 중 하나는 불투명한 삼각형들에 대한 z버퍼링 수행은 삼각형들의 처리 순서가 상관이 없지만 반투명한 삼각형들에 대한 z버퍼링은 뒤에서부터 앞으로 순차적으로 이루어져야만 한다는 차이가 있다. 이를 위해서 반투명한 삼각형들은 정렬 되어야 하지만 간단한 문제가 아니다.

 

 

◾ Z-컬링

z-버퍼링 과정에서 버려지는 프래그먼트들은 앞선 과정에서 텍스처링 작업 등 여러 가지로 수행된 고비용 연산들이 헛수고가 된거나 마찬가지이다.

만약 프래그먼트 처리 단계에 진입하기 전에 버려질 프래그먼트라는 것을 미리 알 수 있다면 즉각적으로 버림으로써 연산량을 크게 줄일 수 있을 것이다. 이 작업은 z-컬링이라고 하고 래스터라이저의 일부 과정이다.

 

z-컬링 알고리즘은 스크린 전체를 감싸는 타일들을 관리하게 된다. 하나의 타일은 n*n 픽셀 영역을 맡게 되고 타일의 픽셀들 중 z값이 가장 큰 값을 저장한다. 이후 래스터라이저가 처리중인 새로운 삼각형이 해당 타일에 놓이는 경우 새로운 삼각형의 세 정점 중 가장 작은 값을 기존에 저장했던 z값과 비교하여 만약 가장 작은 값이 더 크다면 타일에 완벽하게 가려진다는 의미가 되기 때문에 해당 삼각형은 버려지게 된다.

 

만약 씬을 구성하는 삼각형들이 앞에서부터 뒤로 정렬되어 있다면, z-컬링을 통해 성능을 상당히 향상시킬 수 있지만 현실적으로 어려운 문제이기 때문에 대신 물체 단위로 정렬하는 방법을 택한다. 이정도만 해도 임의의 순서로 렌더링 했을 때보다 몇 배는 빨라진다.

 

추가로 오버드로우는 깊이 검사를 통과한 픽셀 수를 스크린의 전체 픽셀 수로 나눈 값이다. 오버드로우가 1에 근접할수록 효율적이라는 의미가 된다.

'이론 > 그래픽스' 카테고리의 다른 글

[Chapter 5] 조명 및 쉐이더  (0) 2023.05.17
[Chapter 3] 래스터라이저  (0) 2023.05.11
[Chapter 2] 정점 처리  (0) 2023.04.27
[Chapter 1] 폴리곤 메쉬  (0) 2022.09.16

클리핑, 원근 나눗셈, 뒷면 제거, 뷰포트 변환, 스캔 변환 등의 요소로 구성된다.

클리핑이나 원근 나눗셈은 알고리즘을 정확히 이해하지 않아도 큰 지장은 없지만 그 외 요소들은 작동 원리에 대한 이해가 있어야 적절한 제어를 할 수 있기 때문에 이해가 필요하다.

특히나 스캔 변환은 잘 이해해야만 프래그먼트 처리를 위한 프로그램을 작성할 수 있다.

 

◾ 클리핑

투영 행렬이 적용된 2x2x1 크기의 클립 공간 바깥에 있는 정점들을 잘라내는 과정이다.

 

 

◾ 원근 나눗셈

각 정점을 자신의 w값으로 나누어서 데카르트 좌표계로 변환시킨다. 보통 NDC(normalized device coordinates)라고 부른다.

w는 카메라 공간의 xy평면으로부터 해당 정점까지의 수직 거리이므로 카메라로부터 멀리 떨어진 물체는 w값이 크기 때문에 w로 나누면 크기가 작아진다.

 

 

◾ 뒷면 제거 (back-face culling)

삼각형의 노말 벡터와 삼각형 정점과 카메라 사이의 정규 벡터를 내적해서 0은 변만 보이는 삼각형, 음수는 앞면, 양수는 뒷면으로 판단할 수 있다.

하지만 컬링은 카메라 공간에서 수행하는 것이 비효율적이기 때문에 투영변환 이후 수행을 하게 된다.

직교투영 이후에는 사실상 z축이 소실되기 때문에 카메라 공간에서처럼 내적으로 판별하지 않고 정점들의 정렬 순서로 판단하게 된다.

 

만약 모든 삼각형의 정점이 시계 방향으로 정렬되어 있다면 반시계 방향은 뒷면으로 판단할 수 있는 것이다.

뒷면으로 판단되었다고 해서 무조건 컬링을 해야하는 것은 아니고 선택적으로 수행할 수 있도록 API에서 지원해준다.

 

차후 다룰 내용이지만 앞면으로 판단되었다고 해서 반드시 최종 렌더링 결과물에 나오는 것은 아니다. 다른 물체들에 의해 가려질 수 있기 때문에 깊이 버퍼라는 알고리즘에 의해 한 번 더 처리가 된다.

 

 

◾ 뷰포트 변환

스크린 상의 윈도우는 y축이 음의 방향인 스크린 공간을 가진다. z축은 화면 안쪽으로 들어가게 된다. (오른손 좌표계)

뷰포트의 종횡비는 절두체의 종횡비와 동일하게 설정된다.

NDC로 표현된 2x2x1 크기의 뷰 볼륨은 뷰포트 변환에 의해 뷰포트로 매핑이 되고, z값은 추후 깊이 버퍼 등에 사용된다.

 

클립 공간은 왼손 좌표계이고 스크린 공간은 오른손 좌표계이기 때문에 xz평면을 기준으로 반사시키고 축소확대, 이동 변환을 적용시키면 뷰포트로의 변환은 끝난다.

 

대부분의 게임에서는 전체화면을 사용하기 때문에 뷰포트가 윈도우 전체 영역을 사용하므로 MinX, MinY는 0이 되고 보통 MinZ, MaxZ는 0, 1로 설정되기 때문에 행렬이 단순화 될 수 있다.

 

뷰포트 변환은 최종적으로 모든 삼각형을 스크린 공간으로 옮기게 된다.

 

 

◾ 스캔 변환

래스터라이저의 마지막 단계이다. 삼각형의 내부를 채우는 프래그먼트들을 생성하는 단계이다.

좀 더 정확하게 말하자면 개별 삼각형이 차지하는 스크린 공간의 픽셀 위치를 결정하고 삼각형의 정점별 속성을 보간하여 이를 각 픽셀 위치에 할당하는 작업이다.

'이론 > 그래픽스' 카테고리의 다른 글

[Chapter 5] 조명 및 쉐이더  (0) 2023.05.17
[Chapter 4] 프래그먼트 처리와 출력 병합  (0) 2023.05.11
[Chapter 2] 정점 처리  (0) 2023.04.27
[Chapter 1] 폴리곤 메쉬  (0) 2022.09.16

+ Recent posts