※ 아래의 내용들은 DirectX 11을 이용한 3D 게임 프로그래밍 입문 책의 내용을 바탕으로 작성된 것입니다.
정점 셰이더
- Direct3D에서 셰이더 프로그램은 주로 효과 파일(effect file)이라고 부르는 텍스트 기반 파일로 작성한다.
- 하나의 정점 셰이더는 본질적으로 하나의 함수이다
- 정점 위치는 정점의 다른 특성들은 관여하지 않는 연산들(ex : 절단)에 쓰이기 때문에, 다른 특성들과는 다른 방식으로 처리해야 한다.
- 기하 셰이더를 사용하지 않는다면 정점 셰이더에서 투영 변환을 반드시 수행해야 한다.
- 기하 셰이더가 쓰이지 않는 경우 하드웨어는 정점 셰이더를 떠난 정점이 동차 절단 공간에 있다고 가정하기 때문
- 기하 셰이더를 사용하는 경우에는 투영 변환을 기하 셰이더에게 미룰 수 있다.
- 정점 셰이더(또는 기하 셰이더)가 원근 나누기까지 수행하지는 말아야 한다.
- 투영 행렬을 곱하는 부분만 책임지면 된다. 원근 나누기는 나중에 하드웨어가 수행한다.
상수 버퍼
- 상수 버퍼(constant buffer)는 셰이더가 접근할 수 있는 다양한 자료를 저장하는 유연한 자료 블록이다.
- 상수 버퍼의 자료는 정점마다 바뀌는 것이 아니다
- 그러나 C++ 응용 프로그램은 효과 프레임워크를 통해서 상수 버퍼의 내용을 실행시점에서 변경할 수 있다.
- 즉, 상수 버퍼는 C++ 응용 프로그램 코드와 효과 코드가 의사소통하는 수단이 된다.
ex: 세계 행렬은 물체마다 다르므로 세계, 시야, 투영 행렬이 결합된 행렬도 물체마다 달라져야 한다. 따라서 앞에 나온 정점 셰이더로 여러 개의 물체를 그리는 경우 각 물체를 그리기 전에 세계 행렬을 적절히 갱신해 주어야 한다.
- 내용을 얼마나 자주 갱신할 것인지에 근거해서 상수 버퍼를 나누어서 만들어야 한다.(230p)
- 상수 버퍼들을 나누는 이유는 효율성 때문이다.
=> 상수 버퍼 하나를 갱신할 때에는 해당 상수 버퍼의 모든 변수를 갱신해야 한다.
=> 따라서 불필요한 갱신을 최소화하려면 변수들을 갱신 빈도에 따라 묶어서 상수 버퍼에 담는 것이 효율적이다.
픽셀 셰이더
- 래스터화기(rasterizer) 단계는 정점 셰이더(또는 기하 셰이더)가 공급한 정점 특성들을 삼각형의 픽셀들을 따라 보간한 결과를 픽셀 셰이더에 입력한다.
- 정점 셰이더처럼 픽셀 셰이더도 본질적으로 하나의 함수이다.
- 중요한 차이는 각 정점이 아니라 각 픽셀 단편마다 실행된다는 점이다.
- 픽셀 셰이더의 주된 임무는 주어진 입력으로부터 픽셀 단편의 색상을 계산해내는 것이다.
- 후면 버퍼의 최종적인 픽셀과, 그 픽셀이 될 가능성이 있는 후보들을 구분하기 위한 용어가 픽셀 단편이다.
- 픽셀 단편이 도중에 기각되어서 후면버퍼까지 도달하지 못할 수도 있다
ex: 픽셀 셰이더에서 잘려 나감, 깊이 판정에 의해 다른 픽셀 단편에 의해 가려짐, 스텐실 판정 등과 같은 이후의 파이프라인 판정 과정에서 폐기됨
- 후면 버퍼의 한 픽셀에 대해 여러 개의 픽셀 단편 후보들이 존재한다.
- 하드웨어 상의 최적화 기법 중 하나로, 파이프라인이 픽셀 단편을 미쳐 픽셀 셰이더에 도달하기 전에 폐기 할 수도 있다. '이른 z 기각(early-z rejection)'
- 이 기법에서 파이프라인은 깊이 판정을 먼저 수행해서, 후면 버퍼의 픽셀에 가려짐이 명백한 픽셀 단편에 대해서는 픽셀 셰이더를 실행하지 않는다.
- 이른 z 기각 최적화가 비활성화 되는 경우도 있다.
ex: 픽셀 셰이더가 픽셀 깊이를 수정하는 경우(이른 z 기각이 일어나는 시점에는 픽셀 셰이더가 픽셀의 깊이를 어떻게 변경할 지 알수 없으므로)
- 픽셀 셰이더의 입력과 정점 셰이더의 출력은 정확히 일치해야 한다.(필수조건)
렌더 상태
- Direct3D는 기본적으로 하나의 상태 기계(state machine)이다.
- Direct3D는 렌더링 파이프라인의 특정 측면의 구성(configuration)에 쓰이는 관련 상태들을 '렌더 상태(render state)'라고 부르는 상태 집합으로 묶어서 관리한다.
- 주요 상태 집합을 대표하는 인터페이스(234p)
- 일반적으로, 응용 프로그램이 실행시점에서 추가적인 렌더 상태 객체를 생성할 필요는 없다.
- 필요한 모든 렌더 상태 객체들을 응용 프로그램의 초기화 시점에서 정의해 두고 사용하면 된다.
- 더 나아가서 일반적으로 그런 렌더 상태 객체를 실행 시점에서 수정할 필요도 없으므로, 렌더링 코드에서 전역 읽기 전용 접근이 가능한 곳에 렌더 상태 객체들을 담아두면 된다.
ex : 모든 렌더 상태 객체를 하나의 정적 클래스 안에 넣어두면 렌더 상태 객체를 중복해서 생성하지 않아도 되고, 렌더링 코드의 여러 부분에서 렌더 상태 객체들을 공유할 수 있다.
효과 프레임워크
- 효과 프레임워크는 특정한 렌더링 효과를 구현하는 데 함께 쓰이는 셰이더 프로그램들과 렌더 상태들을 '효과'라고 부르는 단위로 조직화하고 관리하는 틀을 제공하는 일단의 편의용 코드 집합이다.
- 물, 구름, 금속 물체, 캐릭터 애니메이션 등등을 렌더링 하기 위한 셰이더 프로그램들과 렌더 상태. 각각의 물체(물, 구름, 캐릭터 애니메이션 등)마다 개별적인 효과로 두어서 관리 가능
- 하나의 효과는 적어도 하나의 정점 셰이더와 적어도 하나의 픽셀 셰이더, 그리고 그 효과를 구현하는 데 필요한 렌더 상태들로 이루어진다.
- 셰이더들의 코드를 확장자가 .fx인 효과 파일(effect file)에 담아둔다.
- 하나의 효과 파일은 셰이더 코드와 상수 버퍼의 내용을 담을 뿐만 아니라, 적어도 하나의 기법(technique) 정의도 담는다. 또한, 하나의 기법은 적어도 하나의 패스(pass)를 포함한다.(238p)
- 여러 기법을 하나의 효과 그룹으로 묶을 수도 있다.
- 효과 그룹을 명시적으로 정의하지 않으면 효과 파일 컴파일러는 그 효과 파일에 있는 모든 기법을 담은 하나의 익명 효과 그룹을 생성한다.
- 렌더 상태도 패스의 한 구성요소이므로 효과 파일에서 직접 상태 집합을 생성하고 설정하는 것이 가능하다.
- 이러한 능력은 구체적인 렌더 상태하에서 작동하는 효과에 유용하다.
- 반면, 가변적인 렌더 상태하에서 작동하는 효과들도 있다.
- 상태 전환이 수월하도록 응용 프로그램 수준에서 렌더 상태를 설정하는 것이 낫다.
- 효과를 실제 사용하려면 우선 .fx 파일 안의 셰이더 프로그램들을 컴파일 해야한다.
- D3D11CompileFromFile 함수 사용(6.8.2절 240p)
- 효과 파일의 셰이더들을 성공적으로 컴파일 했다면, 효과 자체를 나타내는 객체(ID3DXEffect11 인터페이스)를 생성한다.
- D3DX11CreateEffectFromMemory() 함수 사용(242p)
- Direct3D 자원의 생성은 비용이 큰 연산이므로 항상 응용 프로그램의 주 루프가 아니라 초기화 시점에서 수행해야 한다.
- 입력 배치나 버퍼, 렌더 상태 객체, 효과들을 항상 초기화 시점에서 생성해야 한다.
- 일반적으로 C++ 응용 프로그램은 효과 객체와 소통할 필요가 있다.
- 보통의 경우 응용 프로그램은 상수 버퍼의 변수들을 갱신해야 한다.
- 효과 객체를 통해서 상수 버퍼 변수에 대한 포인터를 얻을 수 있다.(243p)
- 상수 버퍼의 변수에 대한 포인터를 얻었다면 적절한 C++ 인터페이스를 통해서 변수의 값을 갱신할 수 있다.
- 이 호출들로 효과 객체의 내부 캐시가 갱신되는 것일 뿐, GPU 메모리에 있는 실제 상수 버퍼가 갱신되는 것은 아니다.
- 실질적인 갱신은 렌더링 패스를 수행할 때 일어난다.
- 이러한 방식은 GPU 메모리를 여러 번 조금씩 갱신하는(비효율적) 대신 한 번에 일괄적으로 갱신하기 위한 것이다.
- 효과 변수를 반드시 특화시킬 필요는 없다(244p 참고 내용)
- 임의의 크기의 변수(ex: 범용 구조체)를 설정할 때 유용하다.
- 상수 버퍼 변수들 외에, 렌더링을 수행하려면 효과 객체에 있는 기법 객체를 가리키는 포인터도 얻어야 한다.
- 효과의 한 기법을 이용해서 기하구조를 기리는 과정(245p)
1. 상수 버퍼의 변수들을 적절히 갱신
2. 루프로 기법의 각 패스를 훑으면서 각 패스를 적용해서 기하구조를 그리기
- 효과 파일을 응용 프로그램 빌드 과정에서 컴파일(6.8.5절 246p)
- 셰이더를 컴파일해서 생긴 어셈블리 목록을 점검해 보는 것은 의도하지 않은 명령이 생성되지는 않았는지 정도만 점검하는 수준이라고 해도 유용한 일이다.
- 분기 없이도 같은 결과를 낼 수 있는 경우가 존재하기 때문에 실제로 가끔은 어셈블리 코드를 들여다보고 무슨 일이 벌어지고 있는지 파악하는 것이 좋다.
ex: HLSL 코드에 조건문이 있다면 어셈블리 코드에는 분기 명령이 존재할 가능성이 있다.(GPU상의 분기는 상당히 값비싼 연산)
- 특정 효과를 구현할 때, 사용자의 컴퓨터 성능이 모두 다르기 때문에 저품질, 중간품질, 고품질의 효과를 모두 구현해야 한다.
- 즉, 특정 효과 자체는 하나지만 해당 효과를 구현하기 위해서는 여러 개의 기법이 필요할 수 있다.
- 개별 기법마다 픽셀 셰이더 등이 다를 수 있지만 모든 기법에 공통인 코드 또한 존재할 수 있기 때문에 코드에 중복이 발생한다.
- 조건문 사용(셰이더의 동적 분기에는 추가 부담이 존재하기 때문에 현실적으로는 동적 분기를 꼭 필요한 경우에만 사용해야 한다)
- 필요한 모든 종류의 셰이더 '변이(variation)'들을 컴파일 시점에 생성(251p)
=> 셰이더 코드 자체에는 분기 명령이 필요하지 않다.
틀린 부분이나 이상한 부분이 있으면 댓글로 편하게 지적해주세요.
감사합니다!
'DirectX11 > 정보정리' 카테고리의 다른 글
[DirectX11] 텍스처 적용 (2) | 2022.11.29 |
---|---|
[DirectX11] 조명 (0) | 2022.11.23 |
[DirectX11] 정점 버퍼와 색인 버퍼 (0) | 2022.11.07 |
[DirectX11] 렌더링 파이프라인(2) (0) | 2022.11.05 |
[DirectX11] 렌더링 파이프라인(1) (0) | 2022.11.03 |
댓글