[DirectX11] 12. 변환 – Constant Buffer에 대해 알아보자.



지금까지 렌더링한 사각형을 크기 및 위치를 변화시켜보자. 정점 버퍼의 내용을 수정해도 할 수 있지만, 매번 정점 버퍼의 내용을 수정하면서 렌더링하기엔 꽤나 비효율적이다. 무엇보다 강력한 GPU의 힘을 이용하지 못한다. 정점 버퍼의 내용은 모양을 정의하는 것이다. 이 모양을 Matrix를 통해 변환을 해야한다. 그리고 GPU를 이용하기 위해 해당 변환은 Vertex Shader에서 할 것이다. 이를 위해 필요한 것이 바로 Constant Buffer이다. 이 Constant Buffer에 대해 알아보자.



Screen 좌표



렌더링되고 있는 사각형의 4개의 정점의 값은 위 이미지와 같다. 즉 DirectX11기준으로 스크린 좌표는 중앙이 0,0이고 X, Y축 모두 -1부터 1의 범위값을 가지고 있는 것을 알 수 있다. 따라서 정점 버퍼의 내용만 수정해도 다음과 같이 렌더링 되는 것을 알 수 있다.


이런식으로 변환을 할 수 있지만 서두에 말했다시피 이건 매우 비효율적이기 때문에 Constant Buffer를 통해 쉐이더에서의 변환을 통해 위와 같이 스케일링을 해 볼 것이다.



Constant Buffer의 만들기


Constant Buffer의 만들기도 정점 버퍼인덱스 버퍼와 별반 다르지 않다.

C++
ID3D11Buffer* ConstantBuffer;

D3D11_BUFFER_DESC bd;  
ZeroMemory( &bd, sizeof( D3D11_BUFFER_DESC ) );  
  
bd.Usage          = D3D11_USAGE_DYNAMIC;    
bd.ByteWidth      = sizeof( Matrix );    
bd.BindFlags      = D3D11_BIND_CONSTANT_BUFFER;    
bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
  
ID3D11Device->CreateBuffer( &bd, nullptr, &ConstantBuffer );

역시나 가장 다른건 BindFlagsD3D11_BIND_CONSTANT_BUFFER으로 지정한 것이다. 이 버퍼는 Constant Buffer로 사용하겠다는 의미이다. 그리고 기존에 버퍼들을 생성할때와 설정부분들이 다른게 몇몇 있다. UsageD3D11_USAGE_DYNAMIC으로 지정한 것은 GPU는 읽기만 하고, CPU는 쓰기만 하겠다는 것이다. CPU 쓰기를 위해 CPUAccessFlagsD3D11_CPU_ACCESS_WRITE으로 추가로 지정해주었다. 또한 이 버퍼는 Matrix타입으로 사용하기 때문에 ByteWidthMatrix의 크기로 지정하였다.

지금 사용하는 Constant Buffer의 용도는 정점들을 변환하기 위함이므로 GPU에서는 읽기만 하면 된다. 또한 Draw하기에 앞서 프로그램에서 한번씩만 값을 갱신해주면 된다. 따라서 사용 용도를 D3D11_USAGE_DYNAMIC으로 지정한 것이다.



Rendering Pipeline에 연결하기


생성한 Constant Buffer역시 렌더링 파이프라인에 연결을 해주어야 사용될 수 있다.

C++
ID3D11DeviceContext->VSSetConstantBuffers( 0, 1, &ConstantBuffer );

위 코드는 Vertex Shader스테이지 0번 슬롯에 이 버퍼를 연결하겠다는 뜻이다. DirectX11에서 Constant Buffer는 렌더링 파이프라인의 6개 스테이지에 연결이 가능하다.

연결 함수 이름연결 스테이지
VSSetConstantBuffersVertex Shader Stage
GSSetConstantBuffersGeometry Shader Stage
DSSetConstantBuffersDomain Shader Stage
HSSetConstantBuffersHull Shader Stage
CSSetConstantBuffersCompute Shader Stage
PSSetConstantBuffersPixel Shader Stage

위와 같은 Stage들에서 연결 및 사용이 가능한데 곧 모든 쉐이더 스테이지에서 사용이 가능한 것이다. 그리고 각 스테이지마다 D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT개만큼 연결할 수 있는데 현재 기준으로 14개이다. 다시 정리하면 Constant Buffer는 모든 쉐이더 스테이지에서 14개까지 연결해서 사용이 가능하다. 일단 나는 정점 쉐이더에서 정점의 변환 용도로 버퍼를 사용하므로 VSSetConstantBuffers함수를 통해 연결하였다. 언제나처럼 레퍼런스는 마이크로 소프트에서 참고 가능하다.



Buffer의 내용 갱신하기


일단 버퍼의 내용은 버퍼를 만들면서 지정해줄 수도 있다.

C++
ID3D11Buffer* ConstantBuffer;

D3D11_BUFFER_DESC bd;  
ZeroMemory( &bd, sizeof( D3D11_BUFFER_DESC ) );  
  
bd.Usage          = D3D11_USAGE_DYNAMIC;    
bd.ByteWidth      = sizeof( Matrix );    
bd.BindFlags      = D3D11_BIND_CONSTANT_BUFFER;    
bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;

D3D11_SUBRESOURCE_DATA sd;  
ZeroMemory( &sd, sizeof( D3D11_SUBRESOURCE_DATA ) );

Matrix matrix = DirectX::XMMatrixTranspose( Matrix::CreateScale( 0.5f, 0.5f, 1.f ) );
  
sd.pSysMem = &matrix;  
  
ID3D11Device->CreateBuffer( &bd, &sd, &ConstantBuffer );

이렇게 하면 Identity Matrix로 버퍼의 내용이 갱신된다. 하지만 버퍼의 내용이 변경될 때마다 새롭게 버퍼를 재할당할 수는 없는 노릇이다. 이미 생성된 버퍼의 내용을 프로그램에서 수정할 수 있도록 옵션들을 지정해 생성해 두었다. 생성된 버퍼의 내용을 갱신하는 코드는 다음과 같다.

C++
Matrix matrix = DirectX::XMMatrixTranspose( Matrix::CreateScale( 0.5f, 0.5f, 1.f ) );

D3D11_MAPPED_SUBRESOURCE mappedResource;  
ID3D11DeviceContext->Map( ConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource );  
{  
    memcpy_s( mappedResource.pData, sizeof( Matrix ), &matrix, sizeof( Matrix ) );  
}  
ID3D11DeviceContext->Unmap( ConstantBuffer, 0 );

ConstantBuffer를 우선 Map함수로 D3D11_MAPPED_SUBRESOURCE에 맵핑하여 잡아둔 후 버퍼의 내용을 변경하고 Unmap으로 해제해준다. MapUnmap사이 ConstantBuffer에 GPU는 접근할 수 없다. 자세한 내용은 레퍼런스 MapUnmap을 참고하도록 한다. 여기까지 하면 프로그램 단계에서 버퍼를 사용할 준비를 모두 마쳤다. 아직 쉐이더에서 반영되지 않았기 때문에 이 상태에서 렌더링을 해도 기존과 변화가 없다.



Shader 수정


ConstantBuffer의 용도는 정점 쉐이더에서 정점 변환에 사용하기 위한 것이기 때문에 정점 쉐이더만 변경하면 된다.

HLSL
matrix transform : register( c0 );  
  
PixelIn VS( float4 position : POSITION, float2 texCoord : TEXCOORD )  
{  
    PixelIn output;
    output.position = mul( position, transform );  
    output.texCoord = texCoord;  
  
    return output;  
}

일단 0번 슬롯에 버퍼를 연결했기 때문에 matrix transform : register( c0 );와 같이 레지스터를 이용하여 transform matrix는 0번 슬롯의 Constant Buffer의 내용을 사용한다고 명시하였다. 그리고 정점 쉐이더에서 정점의 위치값에 해당 matrix의 곱을 통해 변환을 해주었다. 이후 렌더링을 하면 다음과 같은 결과를 얻을 수 있다.

Pasted image 20241222223458.png
HLSL
cbuffer TransformBuffer : register( b0 )
{ 
  matrix transform; 
}


여러번 Draw하기


여기에 Constant Buffer의 내용만 갱신하면서 Draw를 여러번 호출할 수 있다.

C++
Matrix matrix = DirectX::XMMatrixTranspose( Matrix::CreateScale( 0.5f, 0.5f, 1.f ) * Matrix::CreateTranslation( -0.5f, 0.5f, 0.0f ) );

D3D11_MAPPED_SUBRESOURCE mappedResource;  
ID3D11DeviceContext->Map( ConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource );  
{  
    memcpy_s( mappedResource.pData, sizeof( Matrix ), &matrix, sizeof( Matrix ) );  
}  
ID3D11DeviceContext->Unmap( ConstantBuffer, 0 );

ID3D11DeviceContext->DrawIndexed( 6, 0, 0 );

matrix = DirectX::XMMatrixTranspose( Matrix::CreateScale( 0.5f, 0.5f, 1.f ) * Matrix::CreateTranslation( 0.5f, -0.5f, 0.0f ) );

D3D11_MAPPED_SUBRESOURCE mappedResource;  
ID3D11DeviceContext->Map( ConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource );  
{  
    memcpy_s( mappedResource.pData, sizeof( Matrix ), &matrix, sizeof( Matrix ) );  
}  
ID3D11DeviceContext->Unmap( ConstantBuffer, 0 );

ID3D11DeviceContext->DrawIndexed( 6, 0, 0 );

IDXGISwapChain->Present( 0, 0 );

이렇게 Present하기전 2번 Draw를 호출하고, Draw를 호출하기전 ConstantBuffer의 내용을 갱신하여 정점 변환 내용을 바꿔주면 아래와 같이 Draw가 누적된 렌더링 결과를 얻을 수 있다.

Pasted image 20241222232217.png


여기까지의 코드는 yunei0313/CRY at Transform-used-constant-buffer에서 볼 수 있다.



이전글 : [DirectX11] 11. 이미지 – Texture2D에 대해 알아보자.
다음글 : [DirectX11] 13. 라이트( Directional Light ) – Constant Buffer 활용하기.



Leave a Comment