Buffers and Shader (1) - Buffers
위의 그림과 같은 상황을 설명해보자.
그리고자 하는 하나의 삼각형이 (x,y,z) 좌표의 형식으로 VERTEX 라는 배열에 준비 되어 있다고 가정하자. 또한 이 정보는 아직 main memory 에 있다.
메인 메모리에 있는 삼각형 정보 좌표를 비디오 메모리의 back buffer에 전달 한 후 back buffer와 front buffer를 교환하면 삼각형이 화면에 나타날 것이다. 앞서 우리는 swap chain 이라는 것을 설명한 적이 있다. 이는 back buffer와 front buffer를 서로 스위칭하여 최신으로 렌더링 된 그림을 화면에 보여주기 위함에 있다. 이런 한번의 스위칭이 하나의 프레임(frame)을 만들게 되는 것이다. 60fps 라는 것은 1초에 이런 스위칭이 60번 이루어 진다는 것을 뜻한다.
만약 삼각형이 실시간적으로 그 좌표가 변화한다면 변경된 좌표는 메인 메모리에 있고 이를 비디오 메모리에 지속적으로 갱신해 주어야 한다.
그러나 만약 삼각형 좌표가 변경되지 않는 정적인 그림 정보라고 한다면 매 프레임마다 메인 메모리의 정보를 비디오 메모리로 전송할 필요가 없을 것이다.
사실 이러한 정적인 그림은 실시간으로 계속 렌더링 할 필요가 없다. 실시간으로 렌더링 한다는 것은 swap chain이 계속 작동한다는 것을 뜻한다.
게임프로그램이든 CAD 프로그램이든, Graphic을 사용하는 모든 프로그램은 이런 정적인 내용만 화면에 보여주는 것은 아니다.
어떤것은 정적으로(배경 화면 같은) 나타나지만 어떤 것은 동적으로 이동이나 회전 등을 하면서 실시간으로 그 좌표가 변경되는 상황이 된다.
그러므로 우리는 화면의 그림중 정적 요소와 동적 요소를 구분할 필요가 있다.
만약 이러한 정적 요소를 비디오 메모리에 넣어 놓고 GPU가 이 정보를 이용해서 렌더링 하도록 하고 여기에 더해서 동적 요소만 메인 메모리에서 비디오 메모리로 전송하여 결합한다면 보다 빠른 성능을 구현할 수 있는 것이다.
직선을 그리는 CAD 프로그램을 생각해 보자.
이미 입력된 직선이 1000 개가 있다고 하고, 이제 마우스를 이용하여 하나의 선을 새로 그리는 동작을 한다고 생각해보자.
그러면 1000개의 직선은 그 좌표가 변경되지 않는다. 지금 그리기 시작한 하나의 직선요소만 실시간으로 좌표가 변경되는 것이다.
화면에 나타나는 그림은 결국 비트맵 형식의 그림이 된다. 아무리 벡터 그래픽이라고 생각하더라도 결국 렌더링 된 이미지는 픽셀 정보를 갖는 이미지인 것이다.
위와 같은 상황을 생각해보면 매 프레임마다 1000번 과 1번의 트렌젝션이 이루어 질 것이다. 이는 CPU와 GPU에 매우 많은 부하를 매 프레임마다 발생 시켜서 성능이 떨어지게 된다.
프로그래머들은 이를 극복하기 위하여 매우 많은 기법을 개발하기도 한다. 그중에 하나가 밉맵(mipmap) 기법이 있다.
이 기법은 이미 입력된 1000개의 직선은 그 좌표가 변경되지 않기 때문에 이들을 이미 그려진 하나의 비트맵 이미지로 만들 수 있다
밉맵에 대한 보다 자세한 내용을 원한다면 아래 위키 링크를 참조하기 바란다.
.
하나의 비트맵 이미지를 비디오 메모리로 전송하는 것은 1000번의 직선을 보내는 것보다 매우 빠르게 이루질 수 있기 때문이다. 즉 한번의 트랜젝션으로 이루어지기 때문에 성능 향상에 많은 발전을 준다.
아래 그림에서 이러한 상황을 도식하여 보여주고 있다.
이러한 방법은 이미 만들어진 비트맵 이미지를 백 버퍼에 전송한 후 새로 그려지는 직선을 백버퍼에 전송한 후 swap chain을 수행하게 되면 새로 갱신된 그림이 화면에 나타나게 될 것이다.
이렇한 방법은 빠른 속도에 비하여 심각한 문제를 발생 시키며 가장 큰 문제는 점점 모니터의 해상도가 커지면서 화면에 그려지는 이미지의 해상도가 커진다는 것이다.
해상도가 높아지면서 비트맵의 메모리를 많이 요구하게 된다는 것은 자명하다.
또한 이러한 비트맵 이미지는 메인 메모리가 갖고 있기 때문에 메모리 메모리 소모가 많다는 것이다. 또한 화면을 확대하고 이동시키고 하는 과정이 들어갈때마다 이러한 비트맵 이미지를 새롭게 갱신해주어야 하기 때문에 어떤 경우에는 앞의 방법보다 더 성능이 떨어지는 상황도 발생할 수 있다.
비디오 메모리는 생각보다 작지 않다. 또한 비디오 메모리는 GPU가 직접 접근할 수 있기 때문에 그 액세스 속도가 매우 빠르다.
벡터 데이터를 이미지화 (Rasterizing) 하는 것 자체가 GPU가 하는 일이기 때문에 차라리 이러한 벡터 그림 정보를 비디오 메모리에 넣어 놓고 GPU가 처리하도록 하는것이 보다 효과적이고 현명한 방법이 된다.
우리는 이러한 방법을 구현하기 위하여 OpenGL에서는 VBO 라는 개념을 준비하게 된 것이다. VBO는 Vertex Buffer Object 의 약자이다. 또한 VAO (Vertex Array Object) 라는 것도 있으며 OpenGL을 처음 배우는 사람이나 어느정도 경험이 있는 사람도 이 둘의 차이를 잘 모를때가 있다.
앞으로 여기에 OpenGL의 VBO에 대한 설명을 할 것이고 이에 대응하여 Direct3D의 Buffer 와 Buffer Descriptor에 대하여 설명할 것이다.
우리는 여기서 다음의 함수를 설명할 것이다.
glGenBuffers
glBindBuffer
glBufferData
glGenBuffers
glGenBuffers — generate buffer object names
Buffer object들을 생성하는 함수이다.
C Specification
void glGenBuffers(GLsizei n, GLuint * buffers);
|
Parameters
n
Specifies the number of buffer object names to be generated.
buffers
Specifies an array in which the generated buffer object names are stored.
Description
glGenBuffers returns n buffer object names in buffers. There is no guarantee that the names form a contiguous set of integers; however, it is guaranteed that none of the returned names was in use immediately before the call to glGenBuffers.
Buffer object names returned by a call to glGenBuffers are not returned by subsequent calls, unless they are first deleted with glDeleteBuffers.
No buffer objects are associated with the returned buffer object names until they are first bound by calling glBindBuffer.
이 함수는 buffers 라는 배열에 buffer object를 생성하여 준다.
buffers 는 GLuint type (실제로 int type 이다) 의 배열의 이름이 된다. n 은 생성하고자 하는 buffer 의 갯수를 뜻한다. 그러므로 buffers 라는 배열은 적어도 n 개 보다는 큰 메모리가 준비 되어 있어야 한다는 것이다.
다음 그림을 보면 이해할 것이다.
GLuint buffers[5];
glGenBuffers(5,buffers); |
5개의 배열 크기를 갖는 buffers 에 5개의 buffer를 생성한 모습이 된다.
buffer0,buffer1,buffer2 등은 buffer id 값이 된다. 이는 비디오 메모리의 특정 위치를 가리키는 메모리 핸들이다. 절대로 직접적인 메모리 주소가 아니고 정수 형태의 handle 값이 된다.우리는 이것을 ID 라고 부르고 buffer id 라 읽을 것이다.
이 buffer id 는 GPU (OpenGL Driver) 가 관리하는 id 인 것이다.
각각의 버퍼 id가 지정하는 버퍼의 크기는 아직 정해지지 않았다. 이는 실제로 버퍼데이터를 입력할 때 정해지게 될 것이다.
만약 버퍼를 하나만 사용하고 싶다면 다음과 같이 하면 된다.
GLuint vbo;
glGenBUffers(1, &vbo) |
glBindBuffer - bind a named buffer object
C Specification
void glBindBuffer( GLenum target, GLuint buffer);
|
Parameters
target
Specifies the target to which the buffer object is bound, which must be one of the buffer binding targets in the following table:
Buffer Binding Target
|
Purpose
|
GL_ARRAY_BUFFER
|
Vertex attributes
|
GL_ATOMIC_COUNTER_BUFFER
|
Atomic counter storage
|
GL_COPY_READ_BUFFER
|
Buffer copy source
|
GL_COPY_WRITE_BUFFER
|
Buffer copy destination
|
GL_DISPATCH_INDIRECT_BUFFER
|
Indirect compute dispatch commands
|
GL_DRAW_INDIRECT_BUFFER
|
Indirect command arguments
|
GL_ELEMENT_ARRAY_BUFFER
|
Vertex array indices
|
GL_PIXEL_PACK_BUFFER
|
Pixel read target
|
GL_PIXEL_UNPACK_BUFFER
|
Texture data source
|
GL_QUERY_BUFFER
|
Query result buffer
|
GL_SHADER_STORAGE_BUFFER
|
Read-write storage for shaders
|
GL_TEXTURE_BUFFER
|
Texture data buffer
|
GL_TRANSFORM_FEEDBACK_BUFFER
|
Transform feedback buffer
|
GL_UNIFORM_BUFFER
|
Uniform block storage
|
buffer target은 많은 종류가 있으나 GL_ARRAY_BUFFER 가 우리가 설명에 필요한 버퍼이다. 그 이유는 우리는 OpenGL Rendering Context에 삼각형과 같은 그림을 그리기 위하여 vertex 정보를 비디오 메모리에 넣어 놓기 위함에 있다.
GLuint vbo;
glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); |
위와 같은 코드를 작성하게 되면 하나의 buffer id 가 준비가 되어 해당 buffer id를 어레이 버퍼에 바인드 시키게 된다.
이렇게 되면 vbo 가 지시하는 버퍼는 array 형식의 메모리로 된다는 것이다. 여전히 해당 버퍼의 크기는 정해지지 않았다.
glBufferData
void glBufferData( GLenum target,
GLsizeiptr size, const GLvoid * data, GLenum usage); |
이 함수에 대한 자세한 형식은 해당 링크를 참조하기 바란다.
이 함수는 버퍼에 값을 넣어주는 함수이다.
target은 앞서 glBindBuffer 에 지정한 buffer target 과 같다.
size 는 data 의 포인터가 갖고 있는 메모리의 byte 크기 이다.
data 는 실제 vertex 정보를 갖고 있는 메모리 포인터 이다.
usage 는
GL_STREAM_DRAW, GL_STREAM_READ, GL_STREAM_COPY, GL_STATIC_DRAW, GL_STATIC_READ,GL_STATIC_COPY, GL_DYNAMIC_DRAW, GL_DYNAMIC_READ, or GL_DYNAMIC_COPY
중 하나의 값이 된다.
usage는 다음 두 그룹의 조합이다.
STREAM,STATIC,DYNAMIC 그룹
DRAW,READ,COPY 그룹
즉 GL_STREAM_DRAW 는 STREAM 과 DRAW 의 조합인 것이다.
역시 GL_STATIC_DRAW 는 STATIC 과 DRAW의 조합이 된다.
usage 의 각각에 대한 설명은 다음에 하기로 하고 여기서는 GL_STATIC_DRAW 를 사용하기로 하자.
STATIC 은 버퍼 데이터가 한번 수정되고(쓰기) 여러번 사용될 경우를 말한다.
또한 DRAW 는 어플리케이션에 의하여 정보가 수정되며 GL drawing 과 이미지 처리 명령에 사용되는 소스라는 것이다.
즉 GL_STATIC_DRAW는 어플리케이션에서 한번 작성되어 저장되고 GL 이 이 정보를 여러번 사용하는 경우를 뜻한다.
이제 코드를 다음과 같이 작성해 보자.
struct VERTEX{float x,y,z;};
VERTEX vertices[] = { {0.0f, 0.5f, 0.0f}, {0.45f, -0.5f, 0.0f}, {-0.45f, -0.5f, 0.0f} }; GLuint vbo; glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); |
VERTEX 는 구조체이며 구조체 하나당 x,y,z 라는 컴포넌트를 갖게 되며 이들은 각각 float 변수 메모리 크기를 갖는다.
위 배열에 있는 좌표에 의한 삼각형은 아래 그림과 같은 꼭지점 좌표를 갖는다. 삼각형의 중심이 (0,0,0) 중심이 된다.
이제 위의 코드에 의하여 수행된 모습은 아래 그림과 같을 것이다.
사실 shader 가 아직 작성 되자 않아서 화면에 표시 된다는 것은 거짓이다.
이후 shader 를 작성하고 swap chain 을 수행한 후 화면에 보여질 것이다.
Direct3D
이제 Direct3D 에서 위와 같은 코딩을 해 보자.
Direct3D 의 초기화 부분은 여기서 설명하지 않는다.
페이지를 참조하기 바란다.
ID3D11Device *dev; // the pointer to our Direct3D device interface
ID3D11Buffer *pVBuffer; // the pointer to the vertex buffer |
Direct3D 는 OpenGL 과 다르게 객체 지향형 라이브러리 이다. 앞서 말했지만 OpenGL 은 C 라이브러리 이고, Direct3D 는 C++ 로 구성된 인터페이스 이다.
또한 OpenGL은 OpenGL Driver 에 의한 State Machine 구조로 작동되나 Direct3D 는 Resource 를 직접 관리 하는 구조로 되어 있다. 그러므로 Direct3D 의 API 호출은 항상 특정한 인터페이스의 멤버 함수를 이용하여 호출 되어 진다.
ID3D11Device interface
위의 링크를 따라가면 해당 인터페이스에 대한 멤버 메소드들을 볼 수 있다.
그중 CreateBuffer 라는 멤버를 살펴보자.
HRESULT CreateBuffer(
[in] const D3D11_BUFFER_DESC *pDesc, [in, optional] const D3D11_SUBRESOURCE_DATA *pInitialData, [out, optional] ID3D11Buffer **ppBuffer ); |
buffer 를 생성하는 멤버 함수이다. 또한 이 버퍼는 vertex buffer, index buffer 또는 shader-constant buffer 중 하나가 될 수 있다.
이 멤버 함수는 해당 device 에 buffer 를 생성하는 기능을 수행한다.
이 함수는 다음과 같은 파라미터들을 필요로 한다.
Parameters
pDesc [in]
Type: const D3D11_BUFFER_DESC*
A pointer to a D3D11_BUFFER_DESC structure that describes the buffer.
pInitialData [in, optional]
Type: const D3D11_SUBRESOURCE_DATA*
A pointer to a D3D11_SUBRESOURCE_DATA structure that describes the initialization data; use NULL to allocate space only
(with the exception that it cannot be NULL if the usage flag is D3D11_USAGE_IMMUTABLE).
If you don't pass anything to pInitialData, the initial content of the memory for the buffer is undefined. In this case,
you need to write the buffer content some other way before the resource is read.
ppBuffer [out, optional]
Type: ID3D11Buffer**
Address of a pointer to the ID3D11Buffer interface for the buffer object created.
Set this parameter to NULL to validate the other input parameters (S_FALSE indicates a pass)
pDesc 는 D3D11_BUFFER_DESC 구조체이다.
이는 buffer 의 생성하는데 있어서 필요한 정보를 담고 있다.
자세한 부분은 API 설명을 보기 바라고 일단 다음과 같이 준비하자.
VERTEX OurVertices[] =
{ {0.0f, 0.5f, 0.0f}, {0.45f, -0.5, 0.0f}, {-0.45f, -0.5f, 0.0f} }; // create the vertex buffer D3D11_BUFFER_DESC bd; ZeroMemory(&bd, sizeof(bd)); bd.Usage = D3D11_USAGE_DYNAMIC; // write access access by CPU and GPU bd.ByteWidth = sizeof(VERTEX) * 3; // size is the VERTEX struct * 3 bd.BindFlags = D3D11_BIND_VERTEX_BUFFER; // use as a vertex buffer bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; // allow CPU to write in buffer dev->CreateBuffer(&bd, NULL, &pVBuffer); // create the buffer // copy the vertices into the buffer D3D11_MAPPED_SUBRESOURCE ms; devcon->Map(pVBuffer, NULL, D3D11_MAP_WRITE_DISCARD, NULL, &ms); // map the buffer memcpy(ms.pData, OurVertices, sizeof(OurVertices)); // copy the data devcon->Unmap(pVBuffer, NULL); // unmap the buffer } |
위의 코드에 의하여 device 에서 vertex buffer 를 생성 한후 vertex 정보를 저장하게 된다.
Direct3D에서는 vertex buffer (OpenGL 에서는 VBO 라고 했다) 를 만들때 그 크기를 정해줘야 한다. 이러한 설정은 D3D11_BUFFER_DESC 구조체를 참조하기 바란다.
이상으로 OpenGL 과 Direct3D의 Vertex Buffer에 대하여 설명 하였다.
이제 Rendering 을 수행하기 위한 Pipeline을 구성해야 한다. Rendering Pipeline은 Shader에 의하여 구성된다.
Shader는 OpenGL 과 Direct3D 모두 지원하며 OpenGL에서는 GLSL 언어를 사용하며 Direct3D에서는 HLSL 언어를 사용한다.
앞으로 이들에 대하여 설명하고자 한다.
댓글 없음:
댓글 쓰기