Constant Buffer

나무늘보·2024년 1월 10일

DirectX

목록 보기
2/5

CreateSRV , IndexBuffer , ConstantBuffer · AshYoon/DirectX-11-3d@db424c0

본격적으로 행렬과 같은 수학을 알아본 다음에 이걸 굉장히 많이 사용할

상수버퍼 Constant Buffer에 대해서 알아봐야한다

Constant Buffer

상수버퍼는 렌더링 파이프라인에서

VS단계에서 우리가 변수를 사용하고싶을때 여기에 꽂아서 붙일수있는 그런 기능

이게 왜 필요한가 ?

Create Geometry를 보면 Position , UV 에 대한 모양 자체를

여기서 우리가 잡아줬었다 , 만약에 우리가 진지한 게임을 만들어서

이게 움직어야하는 Player 같은 존재가된다면 우리가 Update에서 키보드 좌표에 따라서

vertex의 위치를 조정하면 된다 . 아주 미세하게 위 방향으로 조금씩 이동한다던가

Clinet에서 GPU없이 작업할때는 굉장히 단순했었다 . Delta Tick에 * 방향 Vector 곱해서

이동거리 구해준다음에 정점에 더해주기만하면 됬었다

문제가 뭐냐면 Vertex라는걸 처음에 만들때 어떻게 작업을 했는지 살펴보면

Vertex Buffer 을 만들때

//vertex buffer
	{

		D3D11_BUFFER_DESC desc;
		ZeroMemory(&desc, sizeof(desc));
		desc.Usage = D3D11_USAGE_IMMUTABLE; // 세팅해준거 고칠일없어서 immutable
		desc.BindFlags = D3D11_BIND_VERTEX_BUFFER; // vertex buffer bind 용도로 사용할꺼다
		desc.ByteWidth = (uint32)(sizeof(Vertex) * _vertices.size()); // Vertex 메모리 size x vertices vector size()
		// 버퍼 묘사 끝 
		
		D3D11_SUBRESOURCE_DATA data;
		ZeroMemory(&data, sizeof(data));
		data.pSysMem = &_vertices[0]; // vertex 배열의 첫번째의 시작주소 

		// 설정한 값을 기반으로 gpu쪽에 버퍼가 만들어지면서 초기값이 복사가된다 
		//그다음은 gpu만 read only로 작동이된다 , 이게 정점 버퍼 
		HRESULT hr = _device->CreateBuffer(&desc, &data, _vertextBuffer.GetAddressOf()); 
		CHECK(hr);

	}

Vertex Buffer에서 보면 Usage부분에 IMMUTABLE 즉 GPU에서 read Only,

gpu가 write도 안되고 cpu는 접근도 안되는 일회성으로 건내주면 그다음에는 gpu 메모리에서

잘 상주 해가지고 알아서 동작하는 그런 기능인데

만약우리가 위치를 이동해야할때 , vertex의 위치가 바뀔텐데 갱신이 안될것이다

그럼 Vertex Buffer를 다시만드냐 ? 그건아님

1차적으로 생각했을떄 , 여기에 우리가 넘겨주는 VertexBuffer에서 GeoMetry와 관련된

이정보들 , VertexBuffer , IndexBuffer 같은 경우에는 처음에 한번 설정하면 그다음부터는 얘네들이

왜 변화가 없을까 ?

뭘 하길래 ReadOnly로 평생 변하지 않고 사용하겠다라고 일단 선포할까 ??

만약 내가 3D 툴을 이용해서 누가 봐도 이쁜 Player를 제작했다고 했을때

티포즈로 있을것이다 , 이거에 대한 기하학적인 도형에 대한 정보가 방금 봤던 Vertex Buffer에

들어가서 GPU 에서 제적이 되는것 , GPU메모리에 들어가게되는것이고,

앞으로 계속 이동을 할텐데 , 그럼 다같이 모든 정점들이 이동해야하나 ?? 근데 실질적으로

우리가만든 Vertex Buffer에서는 데이터를 복사하는순간 땡 , IMMUTABLE 이 된다

왜 이렇게할까 ? 이 기하학 적인 도형 , 발을 기준으로 했을 때 이 좌표에 대한 모형 자체는

더 이상 변함이 없다는것이고 , 2D게임을 만들때 옆으로 이동을 해야한다면 그게 지금 우리는

전혀 케어가 안된다 .

그렇게하는 이유는 , 우리가 처음에 GPU 에대가 넣어주는 기하학적인 도형은 말그대로

어떤 플레이어의 최초의 기하학적인 모습 그자체를 건내주는것이다

왜냐면 이 모양 자체는 더이상 얘가 애니메이션이 있는게 아니면 즉 비행기라고 가정하면

여기서 얘가 변하는게 아니라 이모양 자체는 변하지않고 , 기하학적인 도형이라 하면

얘가 어떤 기준점을 기준으로 얘가 어떠한 점들어 이루어져 있는지 , 이 사과 자체의 모형은

한번 만들어질때 불변이다 라고 볼수가 있다 .

얘는 모형자체는 그대로 있는거고 우리가 여기에 추가적으로 얼만큼 이동해야하는지를

추가적으로 정보를 받아가지고 그걸 우리가 적용하면 된다는 얘기가된다

이렇게하면 장점이 뭐냐면 , 플레이어를 한명 이렇게 만들어놨는데 MMORPG는 플레이어가 한명이 아니라 1000명 있다고 가정하면 만약에 우리가 처음에 얘기한 방식대로 매 플레이를 좌표가 확정 될때마다 그걸 이용해 가지고 이

Geometry의 좌표를 변신시킨 다음에 그걸 매번마다 우리가 다시 처음에 꽂아준다 , 즉 요 IA단계에서 꽂는다면 속도가 말도안되게 느리지만

vertex buffer를 한번만 만들어주고 , 추가적으로 우리가 VS단계에서 세부적인 아이들의 위치만 조절할수있다면 , 이 기하학적인 도형은 변하지않지만

얘가 얼만큼 이동해야하는지만 추가적인 정보를 넣어주면 그만큼 좌표를 Vertex Shader에서 이동 시키면 굳이 우리가 기하학적인 도형에 손을 대지 않아도된다.

상수 버퍼 마소 공식 홈페이지 설명 및 사용법

https://learn.microsoft.com/ko-kr/windows/win32/direct3dhlsl/dx-graphics-hlsl-constants

상수버퍼 마소 공식 설명

상수버퍼 의 Direct3D 9와 Direct3D 10 및 11의 차이점:

  • 압축을 수행하지 않고 각 변수를 float4 레지스터 집합에 할당하는 Direct3D 9의 상수 자동 할당과 달리 HLSL 상수 변수는 Direct3D 10 및 11의 압축 규칙을 따릅니다.

우리가 Vertex Shader 라고 해서

struct VS_INPUT
{
					  // input layout 작성할때 이름이랑 맞춰주는 이름
	float4 position : POSITION; 
	//float4 color : COLOR;
	float2 uv : TEXCOORD;
};

struct VS_OUTPUT
{
	float4 position : SV_POSITION; //system value , 얘는 필수적으로 있어야한다 라고명시 (SV)
	//float4 color : COLOR;
	float2 uv : TEXCOORD;
};

VS_OUTPUT VS(VS_INPUT input) // 함수 리턴값 함수이름 함수 파라미터 형식 
{
	// 위치와 관련된 부분을 넣어준다고보면되고 , 정점 관련부분 
	VS_OUTPUT output;
	// 일단 인풋 그대로 넘겨주기 

	output.position = input.position;
	output.uv = input.uv;
	//output.color = input.color;
	

	return output;
}

이 단계가 사실상 정보를 꽂아 넣을수가 있다 . Input 단계에서 들어오는건 도형의 모습이고 , 추가적으로 함수에 인자를 받아주는걸

cbuffer TransformData : register(b0)
{
	float4 offset;
}

VS_OUTPUT VS(VS_INPUT input) // 함수 리턴값 함수이름 함수 파라미터 형식 
{
	// 위치와 관련된 부분을 넣어준다고보면되고 , 정점 관련부분 
	VS_OUTPUT output;
	// 일단 인풋 그대로 넘겨주기 

	output.position = input.position + offset;
	output.uv = input.uv;
	//output.color = input.color;
	

	return output;
}

상수버퍼 Constant Buffer 의 약자 cbuffer라고해서 지금은 예시로 TransFormData를 받아 줄껀데 얘는 register(b0) 여기서 b 는 Buffer의 약자

이걸 float 4 개짜리 offset 이라고 받아준 다음에 이걸 cpu가 열심히 세팅해서 TransformData를 넘겨주게된다 . 그러면 실제적으로 Input Position에우리가 방금 받아준 Offset을 , 우리가 Register에 넣어놓기로했기때문에 그대로 사용가능하다

Offset 만큼을 Position에 더해주면된다 . 중요한건 Input position과 Input UV는 이 도형 자체 기하학 적인 도형 자체의 값이기 떄문에 얘가 변하는게 아니라 바로 이 추가적인 constant buffer , 상수 버퍼에다가 우리가 넣어주는값이 이 값이라는 얘기가 된다

그다음 Game.h 에 CreateConstantBuffer라고 상수 버퍼를 만들어주는 함수를 제작해야한다 , init에 추가하는것도 잊지않기

//struct
struct TransformData
{
	Vec3 offset;
	float dummy;
		// constant buffer를 만들때는 16바이트 정렬을 해야되서 더미값을 넣어줘야한다
}

// header 
void CreateConstantBuffer();
private:
	TransformData _transformData;
	ComPtr<ID3D11Buffer> _constantBuffer;

//cpp

이 버퍼에 바로 집어넣는게 아니라 Update 함수를 쓸때 매 프레임마다 나중에 , 이게 사실은 constant buffer에 들어가는 이 트랜스폼 데이터가 사실상 그 플레이엉에 회전값이라던가 소위 나중에가면 이게 그 SRT 라고 하는 Scale Rotation Translation

이 아이의 현재 월드기준의 위치를 여기에 넣어주게 된다 , 지금은 offset이라는 의미없는 값을 넣었지만 나중엔 행렬값이라던지 뭔가를 넣어주게된다 . 지금은 어떤 값을 넣어 놨고 그걸 매 프레임마다 constant buffer라는 상수버퍼에 복사를 해주고싶은데

그걸 어떻게 하면 되는지

→먼저 device context에서 map이랑 unmap이라는걸 세트를 이용하면된다

map을 이용해서 뚜껑을 열어주듯이 우리가 데이터를 넣어줄 준비를 하고 , 그다음 unmap을 해서 뚜껑을 닫는다는 느낌으로 다시 맵했던거를 해제하는 식으로 작업하면된다

void Game::Update()
{
	D3D11_MAPPED_SUBRESOURCE subResource;
	ZeroMemory(&subResource, sizeof(subResource));

	_deviceContext->Map(_constantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &subResource);
	::memcpy(subResource.pData, &_transformData, sizeof(_transformData));
	_deviceContext->Unmap(_constantBuffer.Get(), 0);
}
  • _deviceContext Map 함수에대해서 Direct3D 프로그래밍에서 'map' 및 'unmap' 기능은 CPU에서 버퍼나 텍스처와 같은 리소스의 데이터에 액세스하고 업데이트하는 데 사용됩니다. 이러한 함수는 일반적으로 상수 버퍼, 정점 버퍼, 인덱스 버퍼 및 기타 유형의 버퍼와 연결됩니다.
    1. 지도 기능:

      • map 함수는 리소스에 포함된 데이터에 대한 포인터를 검색하여 CPU가 리소스의 내용을 읽거나 수정할 수 있도록 하는 데 사용됩니다.
      • 매핑할 리소스(버퍼), 하위 리소스 인덱스(종종 단순성을 위해 0으로 설정됨), 요청된 액세스 유형(읽기, 쓰기 또는 읽기-쓰기)과 같은 매개변수를 사용합니다.
      • 일단 매핑되면 포인터를 통해 데이터 작업을 할 수 있습니다. 그러나 충돌을 방지하려면 GPU와 CPU 간의 동기화를 보장하는 것이 중요합니다. 예를 들어 GPU가 현재 리소스를 사용하고 있는 경우 CPU에서 리소스를 매핑하려고 하면 정의되지 않은 동작이 발생할 수 있습니다.
    2. 매핑 해제 기능:
      - unmap 함수는 map 함수를 통해 얻은 포인터를 해제하고 리소스 사용이 완료되었음을 그래픽 API에 알리는 데 사용됩니다.
      - 포인터를 통해 데이터를 수정한 후 GPU에서 렌더링할 수 있도록 리소스 매핑을 해제해야 합니다. 이를 통해 GPU는 다음 렌더링 주기 동안 업데이트된 데이터를 사용할 수 있습니다.
      - 잠재적인 문제를 방지하려면 수정 후 최대한 빨리 리소스 매핑을 해제하는 것이 중요합니다.

      다음은 Direct3D 11을 사용한 간단한 예입니다.

      cppCopy code
      // Assuming pBuffer is an ID3D11Buffer* representing your constant buffer
      
      // Map the constant buffer for writing
      D3D11_MAPPED_SUBRESOURCE mappedResource;
      deviceContext->Map(pBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
      
      // Access the data through mappedResource.pData
      // Modify the data as needed
      
      // Unmap the constant buffer
      deviceContext->Unmap(pBuffer, 0);
      

      이 예에서는 버퍼의 데이터를 완전히 덮어쓰려고 할 때 'D3D11_MAP_WRITE_DISCARD'가 자주 사용됩니다. 적절한 매핑 전략을 선택하는 것은 애플리케이션의 특정 요구 사항에 따라 달라집니다.

중요한건 map 을하고 unmap이라는 함수를 호출하고 그 사이에 데이터를 복사해주면된다 .

즉 memcpy를 해서 subresource.pData를 하면 뚜껑이 열린상태로 데이터를 복사하고 뚜껑을 닫으니 여기있는 데이터가 복사가된다 , 어떤 데이터를 복사하고싶냐 → transformdata를 복사하고싶다 라는 느낌

의미있는 값을 넣어준다면 ,

_transformData.offset.x = 0.3f;
_transformData.offset.y = 0.3f;

x 랑 y 가 각각 0.3 이기때문에 쉐이더에 받아줄때 offset을 더해준다고했으니깐 position이 실제로 변하게된다는말

실제적으로 우리가 이 위치를 조절해줄수있다는말

늘그렇듯 constant buffer를 만들었으면 우리가 설정을 해줘야하는데

void Render()
{
	//IA -
//VS

		_deviceContext->VSSetShader(_vertexShader.Get(), nullptr, 0); // 우리가만든거 써라 
		_deviceContext->VSSetConstantBuffers(0, 1, _constantBuffer.GetAddressOf());

// RS - PS - OM 
}

0 번 슬롯 사용하고 1개만 사용할것이고 , constantbuffer의 get addressof 해주면 된다

결국 Comptr _constantBuffer 는 gpu쪽에있는 buffer는 맞긴한데 우리가 cpu에서 gpu 쪽으로 데이터를 싹 밀어넣어준거고

Set ConstantBuffer를 통해서 Rendering pipe Line 단계에서 묶어준 상태이기 때문에 쉐이더에서

cbuffer TransformData : register(b0)
{
	float4 offset;
}

이 cbuffer 가 문제가 없다면 사용할수있는 준비가 끝났다는 얘기가 된다

그럼이 float4 offset을 받았다면 얘를 vertex Shader 단계에서 사실상 사용하고 있는것이니

Untitled

전 과 달리 확실히 움직인걸 볼수있다

그렇다는걸 우리가 매 프레임마다 offset 값을 변화하게 만들어주면 ?

_transformData.offset.x += 0.0003f;
_transformData.offset.y = 0.3f;

왼쪽에서 오른쪽으로 서서히 움직이게 된다

그래서 중요한건 결국 처음에 우리가 넣어준 vertex input에서 넣어준거는 기하학적인 해당 물체의 고유 도형을 얘기하는것이기 떄문에 걔를 절대 수정하면 안되고 걔를 냅두고 추가적인 정보만 넣어서 물체를 이동시켜야 되는게 핵심이다

요약

VS단계에서 함수에 인자를 넣듯 , 변수를 건내주는것처럼 데이터를 넣어줄수있다는 얘기가 된다 . 그럼 애당초 vertex buffer에서 값을 수정하지않고 왜 굳이 상수 버퍼를 이용해야되는건가 라는걸 잘 이해해야한다

메쉬는 틀리고 움직이는것은 메쉬를 얼마만큼 움직일지를 추가적으로 해줘야하는것인데 . 만약에 우리가 vertex를 우리가 움직일때마다 다시 계산해서 다시 넣어준다는건

우리가 만약 max같은 툴을 통해서 삼각형이 30만개가 있다면 30만개를 대상으로 다시한번 덧셈 연산을하고 그다음에 30만개를 다시 gpu한테 던져줘가지고 처음에 우리가만들었던 vertex buffer 를 다시 만든다는 개념이된다

→ 이게 말도 안됀다는 뜻

메시는 동일하게 있고 그거에대한 상대적은 offset 좌표만 다르게해서 이동시켜줘야한다는뜻 ,

지금은 offset 밖에없지만 나중가선 SRT , Scale Rocation , Translation 뭐 돌리고 이동하고 이런 정보들을 들고있을 것인데 그걸 이유해가지고 우리가 vs 단계에서 이런식을 수정을한다 라는얘기

vertex buffer 는 read only로 만든 이유가있다 - > 얘를 고치는게아니라 constant buffer에 추가적인 인자를 넣어줘서 vs에서 합쳐서 고쳐야한다 몬스터가 1000마리건 10,000 마리건 cpu에서 gpu로 한번 힘들게 복사한 그정보를 재탕해서

사용하면되는 것이고 더이상 손볼 필요가없다는뜻 그 이후로는 상수 버퍼에다가 우리가 추가적으로 바꾸고 싶은 인자만 넣어가지고 동작시키면된다

UE 에서 머테리얼이라는 재질에 대해서 우리가 추가적인 옵션같은걸 설정할수있긴하다 , 그런 세부적인 설정값이 상수버퍼에 들어간다고 보면된다 . 전체적인 틀에서 이 vertex buffer를 건드리는건 아니다

이렇게 상수 버퍼를 사용할 준비가 끝났으니 여기에 추가적인 행렬값 또는 수학공식을 넣어서 실질적으로 쉐이더에 적용을 시켜서 이 물체가 어디로 이동하거나 3배 커진다거나 회전한다거나얘를 잘 조작해주면된다

이런 Transform 정보를 VS 단계에서 꽂아준다 라고 보면된다

핵심은 매프레임마다 devicecontext 에서 map과 unmap으로 데이터를 복사해서 넣어준다

void Game::Update()
{
	// Scale Rotation Translation
	_transformData.offset.x += 0.0003f;
	_transformData.offset.y = 0.3f;

	D3D11_MAPPED_SUBRESOURCE subResource;
	ZeroMemory(&subResource, sizeof(subResource));

	_deviceContext->Map(_constantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &subResource);
	::memcpy(subResource.pData, &_transformData, sizeof(_transformData));
	_deviceContext->Unmap(_constantBuffer.Get(), 0);
}

복습이 매우 중요하다

profile
Unreal Programmer , C++

0개의 댓글