학습 페이지
www.inflearn.com

오늘은 충돌과 피킹과 같은 요소를 다루기 전에 기본기를 다뤄보자
컴퓨팅쉐이더
1.이론
컴퓨팅쉐이더에 대해 알아보자. 이 쉐이더를 통해 cpu가 아닌 gpu에 일을 어떻게 넘기는지 그 방식에 대해 알 수 있다.
우리가 지금까지 작업해준 메인 코드는 모두 CPU에서 돌아가는 코드이고 엔진쪽에서 DX에 의해 처리되고 있는 버퍼와
같은 코드들이 GPU에 의해 처리된다. 결과물이 렌더링 파이프라인을 통해서 GPU 를 통해 렌더링된다고 보면된다.
GPU에게 일을 시키기 위해 쉐이더파일(.fx)을 만들어줬다.
GPU는 연산에 특화되어있다. 병렬로 처리할 수 있는 단순 작업을 CPU를 대신하여 GPU에서 처리를 해주면 좋은데
이렇게 일반적인 그래픽스 용도가 아닌 범용적인 용도로 GPU를 사용하는 것을 GPGPU라고 한다.
DX에서도 결국에는 컴퓨팅쉐이더를 이용해서 일반적인 쉐이더가 아니라 GPU에 단순 연산을 시키는 방식을 이용할 수 있다. 이러한 방식을 텍스처를 조작해서 다른 파일을 만들거나 하나의 스트림을 압축하거나 애니메이션에서 순간적인 상태의 애니메이션 위치를 알 수 있는 처리에 사용할 수 있다.
현재는 공용정보가 있다면 락을 거는 방식을 사용해서 다수의 스레드를 적용시켜 멀티셋 환경에서 작업을 할 수 있지만 이 작업 GPU를 통해 이제 해주어야한다.
일단 이 컴퓨팅 쉐이더를 사용하면서 배워보자. 핵심은 GPU에 어떤식으로 일을 분배할 것인가 이다.
먼저 GPU에 데이터를 넘겼다가 다시 받으려면 리소스가 필요하다.
여러 리소스의 종류 중에 지금은 Raw Buffer(Byte Address Buffer)를 사용해보자. 이 Raw Buffer는 주소를 이용해서
우리가 직접 주소값을 연산해서 접근하는형태로 바이트형 포인터를 다루는 느낌이라고 생각하면 된다. 이것도 리소스이기 때문에 리소스를 만들어주고 이를 묘사하는 view를 만들어주어야 한다. 실질적으로 GPU와 통신할 때는 이 View의 정보를 건네주고 이를 바탕으로 작업을 하고 받아 올때도 View를 통해 컴퓨팅 쉐이더에 건내준 다음 임시 버퍼를 통해 결과값을 취합한다.
2.코드
우선 RawBuffer 클래스를 만들어주자. 우리가 이 클래스로 입력을 받아주고 컴퓨팅 쉐이더에 연산을 요청한 다음 이 결과값을 받아주면 된다.
이를 위해 입력값을 받아주는 변수는 어떤값이 들어올지 모르기 때문에 보이드형 포인터로 선언해주자. 이는 cpu 메모리에 있는 정보이다. 예를 들어 벡터가 있다면 벡터의 첫번째 데이터의 주소를 주고 이를 통해 크기정보를 받아온다.
그리고 입출력의 크기값을 저장할 변수를 만들어주고 입력값은 생성자에서 받아주자. 이 입력값을 바탕으로 지정한 크기의 버퍼를 만들고 작업해주면 된다.
View를 통해 받아온 정보를 꺼내주기 위한 버퍼도 만들어주자.
결과값도 View에서 바로 가져올 수 는 없고 임시 버퍼를 통해 복사 한 다음 작업을 해서 가져와야한다.
버퍼를 만든다음 cpu 메모리에서 데이터를 만들어주면 그것을 건네주는 함수와 컴퓨팅 쉐이더를 통한 호출이 완료된 다음에 임시 버퍼의 값을 가져오는 함수를 만들어주자. 이 과정은 맵과 언맵을 통해 해준다.
RawBuffer.h
#pragma once
class RawBuffer
{
public:
RawBuffer(void* inputData, uint32 inputByte, uint32 outputByte);
~RawBuffer();
public:
void CreateBuffer();
//입력값 넣어주기
void CopyToInput(void* data);
//결과값 가져오기
void CopyFromOutput(void* data);
public:
ComPtr<ID3D11ShaderResourceView> GetSRV() { return _srv; }
ComPtr<ID3D11UnorderedAccessView> GetUAV() { return _uav; }
private:
//버퍼만들기
void CreateInput();
//입력 SRV만들기
void CreateSRV();
//결과가져올버퍼 만들기
void CreateOutput();
//결과 가져올 UAV
void CreateUAV();
//결과값 취합 및 복사
void CreateResult();
private:
ComPtr<ID3D11Buffer> _input;
ComPtr<ID3D11ShaderResourceView> _srv;
ComPtr<ID3D11Buffer> _output;
ComPtr<ID3D11UnorderedAccessView> _uav;
//결과값을 복사해올 버퍼
ComPtr<ID3D11Buffer> _result;
private:
void* _inputData;
uint32 _inputByte = 0;
uint32 _outputByte = 0;
};
RawBuffer.cpp
#include "pch.h"
#include "RawBuffer.h"
RawBuffer::RawBuffer(void* inputData, uint32 inputByte, uint32 outputByte)
:_inputData(inputData),_inputByte(inputByte),_outputByte(outputByte)
{
CreateBuffer();
}
RawBuffer::~RawBuffer()
{
}
void RawBuffer::CreateBuffer()
{
CreateInput();
CreateSRV();
CreateOutput();
CreateUAV();
CreateResult();
}
void RawBuffer::CopyToInput(void* data)
{
D3D11_MAPPED_SUBRESOURCE subResource;
//맵
DC->Map(_input.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &subResource);
{
memcpy(subResource.pData, data, _inputByte);
}
//언맵
DC->Unmap(_input.Get(), 0);
}
void RawBuffer::CopyFromOutput(void* data)
{
// 출력 데이터 -> result에 복사
DC->CopyResource(_result.Get(), _output.Get());
D3D11_MAPPED_SUBRESOURCE subResource;
DC->Map(_result.Get(), 0, D3D11_MAP_READ, 0, &subResource);
{
memcpy(data, subResource.pData, _outputByte);
}
DC->Unmap(_result.Get(), 0);
}
void RawBuffer::CreateInput()
{
if (_inputByte == 0)
return;
D3D11_BUFFER_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.ByteWidth = _inputByte;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS; // RAW_BUFFER
desc.Usage = D3D11_USAGE_DYNAMIC; // CPU-WRITE, GPU-READ
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
D3D11_SUBRESOURCE_DATA subResource = { 0 };
subResource.pSysMem = _inputData;
if (_inputData != nullptr)
CHECK(DEVICE->CreateBuffer(&desc, &subResource, _input.GetAddressOf()));
else
CHECK(DEVICE->CreateBuffer(&desc, nullptr, _input.GetAddressOf()));
}
void RawBuffer::CreateSRV()
{
if (_inputByte == 0)
return;
D3D11_BUFFER_DESC desc;
_input->GetDesc(&desc);
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
ZeroMemory(&srvDesc, sizeof(srvDesc));
//일종의 널포인터
srvDesc.Format = DXGI_FORMAT_R32_TYPELESS; // 쉐이더에서 알아서 하세요
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFEREX; // SRV_FLAG_RAW
srvDesc.BufferEx.Flags = D3D11_BUFFEREX_SRV_FLAG_RAW;
srvDesc.BufferEx.NumElements = desc.ByteWidth / 4; // 전체 데이터 개수 - 전체 / 하나=4
CHECK(DEVICE->CreateShaderResourceView(_input.Get(), &srvDesc, _srv.GetAddressOf()));
}
void RawBuffer::CreateOutput()
{
D3D11_BUFFER_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.ByteWidth = _outputByte;
desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS;
desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS;
CHECK(DEVICE->CreateBuffer(&desc, NULL, _output.GetAddressOf()));
}
//인풋SRV와 대칭적
void RawBuffer::CreateUAV()
{
D3D11_BUFFER_DESC desc;
_output->GetDesc(&desc);
D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
ZeroMemory(&uavDesc, sizeof(uavDesc));
uavDesc.Format = DXGI_FORMAT_R32_TYPELESS;
uavDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
uavDesc.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_RAW;
uavDesc.Buffer.NumElements = desc.ByteWidth / 4;
CHECK(DEVICE->CreateUnorderedAccessView(_output.Get(), &uavDesc, _uav.GetAddressOf()));
}
void RawBuffer::CreateResult()
{
D3D11_BUFFER_DESC desc;
_output->GetDesc(&desc);
desc.Usage = D3D11_USAGE_STAGING;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
desc.BindFlags = D3D11_USAGE_DEFAULT; // UAV가 연결되려면, USAGE는 DEFAULT여야 함.
desc.MiscFlags = 0;
CHECK(DEVICE->CreateBuffer(&desc, nullptr, _result.GetAddressOf()));
}
이렇게 만들어준 버퍼를 메인쪽에서 사용할 수 있도록 코드를 구현해보자. 일단은 Input 없이 Output만 받아주는 방식으로 만들어주자. 먼저 컴퓨트 쉐이더를 사용하여 연산을 해줄 shader 코드를 구현하자. 이때 output의 offset은 받아주는 구조체의 크기와 바이트 개수를 통해 계산한다. 이를 통해 데이터가 연속해서 배열로 있다면 그 데이터를 캐릭터 / 바이트 포인터로 연산해서 주소를 찾아가야 한다면 몇번째 데이터인지 각 데이터의 크기를 통해 offset을 정해줘서 넘어갈 수 있다. 결국 이 쉐이더 코드는 CPU에서 하는 작업으로 치자면 구조체를 만들고 이 구조체변수에 값을 할당해주는 작업이라고 보면 된다.
RawBufferDemo.fx
#pragma once
#include "IExecute.h"
class RawBufferDemo : public IExecute
{
struct Output
{
uint32 groupID[3];
uint32 groupThreadID[3];
uint32 dispatchThreadID[3];
uint32 groupIndex;
};
public:
void Init() override;
void Update() override;
void Render() override;
private:
shared_ptr<Shader> _shader;
};
이제 메인함수에서 작동하는 지 확인해보기 위해 Output으로 받을 구조체와 쓰레드 개수와 결과값을 받기위해 UAV 설정을 해주고 이를 확인하기 위해 엑셀파일로 파일 출력을 해주도록 하자.
RawBufferDemo.h
#pragma once
#include "IExecute.h"
class RawBufferDemo : public IExecute
{
struct Output
{
uint32 groupID[3];
uint32 groupThreadID[3];
uint32 dispatchThreadID[3];
uint32 groupIndex;
};
public:
void Init() override;
void Update() override;
void Render() override;
private:
shared_ptr<Shader> _shader;
};
RawBufferDemo.cpp
#include "pch.h"
#include "RawBufferDemo.h"
#include "RawBuffer.h"
void RawBufferDemo::Init()
{
_shader = make_shared<Shader>(L"24. RawBufferDemo.fx");
//하나의 쓰레드 그룹 내에서 운영할 쓰레드 개수
uint32 count = 10 * 8 * 3;
shared_ptr<RawBuffer> rawBuffer = make_shared<RawBuffer>(nullptr, 0, sizeof(Output) * count);
//결과 묘사하는뷰 - 결과 값을 받을 UAV
_shader->GetUAV("Output")->SetUnorderedAccessView(rawBuffer->GetUAV().Get());
// x, y, z => 쓰레드 그룹
_shader->Dispatch(0, 0, 1, 1, 1);
vector<Output> outputs(count);
rawBuffer->CopyFromOutput(outputs.data());
FILE* file;
::fopen_s(&file, "../RawBuffer.csv", "w");
::fprintf
(
file,
"GroupID(X),GroupID(Y),GroupID(Z),GroupThreadID(X),GroupThreadID(Y),GroupThreadID(Z),DispatchThreadID(X),DispatchThreadID(Y),DispatchThreadID(Z),GroupIndex\n"
);
for (uint32 i = 0; i < count; i++)
{
const Output& temp = outputs[i];
::fprintf
(
file,
"%d,%d,%d, %d,%d,%d, %d,%d,%d, %d\n",
temp.groupID[0], temp.groupID[1], temp.groupID[2],
temp.groupThreadID[0], temp.groupThreadID[1], temp.groupThreadID[2],
temp.dispatchThreadID[0], temp.dispatchThreadID[1], temp.dispatchThreadID[2],
temp.groupIndex
);
}
::fclose(file);
}
void RawBufferDemo::Update()
{
}
void RawBufferDemo::Render()
{
}
이렇게해서 실행해주면 엑셀파일이 하나 생긴다.

지금은 규칙과 의미를 찾기가 힘들지만 앞으로 계속 활용해보면서 의미를 찾아나가야한다.
컴퓨트 쉐이더를 실행해서 쓰레드를 사용해서 일을 시켜야한다. CPU에서는 Thread를 직접 관리해서 만든 다음에
공용데이터를 이용하거나 일정 부분의 처리를 맡겼는데
GPU에서는 Thread가 많기 때문에 각각 지정해주는 것 보다
는 어떤 규칙성을 가지고 하는 것이 합리적이다. 지금은 보면 고유하게 넘버링이 있다는 것을 알면 된다. 우리가 지정해준
쓰레드 개후에 따라서 일관적인 규칙으로 넘버링이 붙는다.
이 중에 어떤 정보를 가지고 어떤 조작을 하는지에 따라 가공한 정보를 위처럼 몇번째 칸에 넣어주는지를 정해줄 수 있는 것이다.
3. System Value 해석
이제 이 값이 어떤의미가 있는지 알아보자. 비유로 표현해보자면 칸 하나가 병사(쓰레드)라고 보면 된다.
우리는 이런 병사가 240개를 지정해준 것이고 이 병사를 다 모아서 하나의 그룹이 되는 것이고 그 그룹이 몇개 있는지
Dispatch함수를 통해 3차원으로 만들어주는 것이다.

이제 이SV값이 무엇을 의미하는지 알아보자.
Group ThreadID는 하나의 그룹내에서 나의 쓰레드 ID는 몇번이지를 나타낸다. 이 값은 그룹내에서는 고유하다.
GroupID는 몇번째 그룹인지를 나타낸다.
Dispatch ThreadID는 자신이 몇번째인지 그룹자체의 번호와 해당하는 그룹에서 몇번인지를 구해주는 것으로 이 값은 모든 스레드마다 고유하게 있을 것이다.
GroupIndex는 Group ThreadID의 값을 활용해서 앞자리에 둔다. 7*1 + 5*10 + 0*10*8 끝에 10*8은 10,8,3에서 나온 것이다. 2차원배열을 1차원으로 만들 때 하나는 그냥 더해주고 하나는 사이즈에 곱해서 더해주는 식의 방식과 같이 인덱스를 표현해준 것이다. 결국에는 하나의 그룹내에서 몇번째 쓰레드인지 나타낸다.
이것이 중요한 이유는 어떻게 데이터를 분할해서 시킬지에서 컴퓨트 쉐이더를 이용하는 경우도 있고 행렬을 이용해서 그 행렬을 여러 개의 쓰레드들이 담당하게 만들어 줄 수 있는 등의 다양한 형태로 분배를 해줄텐데 어떤 애가 어떤애를 참고할지는 이 SV값을 따라서 분할해서 일감을 주면 된다.
실습2
지금은 Output만 추출해주고 있지만 원래는 Input을 받아서 가공해서 다시 뱉어주는 것이 일반적이다.
랜덤 값을 하나 정해준 다음 인풋에 넣어서 건네주고 이를 통해 쉐이더 파일을 거친 다음에 읽어오는 Read와 Write를 같이 한번 해보자.
결국 굉장히 많은 데이터를 건네줬을 때 특정 쓰레드가 담당하는 그 공간에 있는 인풋을 가져와서 복사한 뒤 반환하는 것을 보고 싶은 것이다.
일단 입력 구조체를 만들고 값을 정해주자.
GroupDemo.h
#pragma once
#include "IExecute.h"
class GroupDemo : public IExecute
{
struct Input
{
float value;
};
struct Output
{
uint32 groupID[3];
uint32 groupThreadID[3];
uint32 dispatchThreadID[3];
uint32 groupIndex;
float value;
};
public:
void Init() override;
void Update() override;
void Render() override;
private:
shared_ptr<Shader> _shader;
};
GroupDemo.cpp
#include "pch.h"
#include "GroupDemo.h"
#include "RawBuffer.h"
void GroupDemo::Init()
{
_shader = make_shared<Shader>(L"25. GroupDemo.fx");
//하나의 쓰레드 그룹 내에서 운영할 쓰레드 개수
uint32 threadCount = 10 * 8 * 3;
uint32 groupCount = 2 * 1 * 1;
//최종 데이터개수 - 그룹 수 * 그룹내 쓰레드의 수
uint32 count = groupCount * threadCount;
vector<Input> inputs(count);
for (int32 i = 0; i < count; i++)
inputs[i].value = rand() % 10000;
shared_ptr<RawBuffer> rawBuffer = make_shared<RawBuffer>(inputs.data(), sizeof(Input) * count, sizeof(Output) * count);
_shader->GetSRV("Input")->SetResource(rawBuffer->GetSRV().Get());
//결과 묘사하는뷰 - 결과 값을 받을 UAV,
_shader->GetUAV("Output")->SetUnorderedAccessView(rawBuffer->GetUAV().Get());
// x, y, z => 쓰레드 그룹
_shader->Dispatch(0, 0, 2, 1, 1);
vector<Output> outputs(count);
rawBuffer->CopyFromOutput(outputs.data());
FILE* file;
::fopen_s(&file, "../RawBuffer.csv", "w");
::fprintf
(
file,
"GroupID(X),GroupID(Y),GroupID(Z),GroupThreadID(X),GroupThreadID(Y),GroupThreadID(Z),DispatchThreadID(X),DispatchThreadID(Y),DispatchThreadID(Z),GroupIndex,Value\n"
);
for (uint32 i = 0; i < count; i++)
{
const Output& temp = outputs[i];
::fprintf
(
file,
"%d,%d,%d, %d,%d,%d, %d,%d,%d, %d,%f\n",
temp.groupID[0], temp.groupID[1], temp.groupID[2],
temp.groupThreadID[0], temp.groupThreadID[1], temp.groupThreadID[2],
temp.dispatchThreadID[0], temp.dispatchThreadID[1], temp.dispatchThreadID[2],
temp.groupIndex,temp.value
);
}
::fclose(file);
}
void GroupDemo::Update()
{
}
void GroupDemo::Render()
{
}
그리고 Output만 있던 쉐이더에서 Input을 받을 수 있도록 수정해주자. ByteAddressBuffer로 입력을 정의해주고
그룹을 2개로 늘려줬기 때문에 이에 맞게 1차원에서 봤을 때의 각 쓰레드의 유니크한 인덱스를 계산해주고
이를 데이터 자료형크기에 따라 곱해준다음 그 주소값에서 입력 값을 가져와서 반환해주자.
GroupDemo.fx
ByteAddressBuffer Input; // SRV
//Read + Write
RWByteAddressBuffer Output; // -> UAV
struct ComputeInput
{
//SV: system Value 지정되어 있는 시스템 벨류
uint3 groupID : SV_GroupID;
uint3 groupThreadID : SV_GroupThreadID;
uint3 dispatchThreadID : SV_DispatchThreadID;
uint groupIndex : SV_GroupIndex;
};
//thread 개수 a*b*c 총합
[numthreads(10,8,3)]
void CS(ComputeInput input)
{
//2차원 배열에서 유니크한 인덱스 계산 몇번째인지 2차원 -> 1차원
uint index = input.groupID.x * (10 * 8 * 3) + input.groupIndex;
//10(3+3+3+1+1) : 구조체 크기, 4: 바이트
uint outAddress = index * 11 * 4;
//데이터 : 4바이트 * 몇번째인지
uint inAddress = index * 4;
float value = (float)Input.Load(inAddress);
//구조체에서 각 변수가 떨어진 정도
Output.Store3(outAddress + 0, input.groupID);
Output.Store3(outAddress + 12, input.groupThreadID);
Output.Store3(outAddress + 24, input.dispatchThreadID);
Output.Store3(outAddress + 36, input.groupIndex);
Output.Store3(outAddress + 40, (uint)value);
}
technique11 T0
{
Pass P0
{
SetVertexShader(NULL);
SetPixelShader(NULL);
//컴퓨트 쉐이더 세팅
SetComputeShader(CompileShader(cs_5_0, CS()));
}
};
그렇게 해주면 Value값이 더해진 엑셀파일이 생성된다.

이 엑셀파일을 내리다보면 240번째가 넘어서면 x값이 1이 증가하는 것을 볼 수 있다.
쓰레드를 지정해줄 때 10,8,3 처럼 3차원으로 해주는 이유는 x y z를 통한 넘버링으로 이 값을 조정하면서
우리가 원하는 타입에 해당하는 정보를 빠르게 추출할 수 있다.
지금처럼 ByteAddressBuffer를 통해 offset 주소를 계산해주면 정해지지 않은 임의의 형태의 데이터를 넣어줄 수 있지만
다음에 배울 Structured Buffer를 이용해서 정해진 구조체를 통한 배열을 만들어서 관리하는 버퍼를 통해 더 쉽게 이 작업을 해줄 수 있다.
'게임공부 > Directx11' 카테고리의 다른 글
[Directx11][C++][3D]24. StructureBuffer (0) | 2024.10.02 |
---|---|
[Directx11][C++][3D]23. TextureBuffer (4) | 2024.10.02 |
[Directx11][C++][3D]21. Quaternion (0) | 2024.09.29 |
[Directx11][C++][3D]20. 통합 & 정리 (1) | 2024.09.28 |
[Directx11][C++][3D]19. 인스턴싱(ModelAnimator) (0) | 2024.09.26 |