프로그래밍 공부
작성일
2023. 12. 14. 17:07
작성자
WDmil
728x90

컴퓨터 쉐이딩

 

우선, 컴퓨터 쉐이딩을 알기 위해서는 스레드멀티프로세스를 이해해야한다.

 

스레드는 작업 한개를 처리하는 단위이고,

 

멀티프로세스는 멀티스레드 를 사용해서 동작하는 프로세스를 여러개 돌린다고 이해하면 된다.

 

DriectX에서 스레드와 뮤텍스 라이브러리로 다중 스레드 프로그래밍을 사용할 수 있다.

쓰레드와 뮤텍스 라이브러리로 다중쓰레드를 사용할 수 있다.

#include <thread>
#include <mutex>

#include <DirectXCollision.h>

DirectXCollision은, 비효율적이지만, 다중처리방식을 사용하면 큰 부하를 일으키지 않고 사용할 수 있다.

 


위와같은 멀티프로세스 방식의 코딩을 사용하면, 비동기 방식의 데이터처리 시 더 효율적으로 사용할 수 있다.

 

예를들어 플레이어가 아이템을 주워서 인벤토리에 넣었다고 가정해보자.

 

그렇다면, 플레이어가 아이템을 주웠다는 정보를 서버에 넘기고, 서버는 데이터를 받고. 잘받았다고 리턴하고.

 

그 아이템을 인벤토리로 넣었다는 정보를 서버는 플레이어에게 넘기고, 플레이어는 잘 받았다고 리턴하고.

 

를 반복하는동안, 게임은 정지할것이다.

 

실제로 큰 시간차이가 발생하지는 않겠지만, 플레이 시 부드러운 게임플레이를 보장할 수 없고.

 

데이터가 처리되는 동안 게임은 정지할 것 이고 모든 인벤토리 처리가 완료된 뒤에 게임은 움직일 것이다.

 

이러한 과정이 이루어지는 동안. 우리는 게임이 잠깐씩 멈춘다고 느낄수 있다.

 

여기에 멀티프로세스 방식을 사용하면

 

아이템을 줍는다 -> 주웠다는 데이터를 서버에 넘기고 받는 프로세스를 가동한다.

         ㅣ                                                                  ㅣ

아이템을 줍고 행동한다.   ㅣ   아이템을 플레이어 인벤토리에 넣는 작업을 한다.

         ㅣ                                                                 ㅣ

플레이어는 행동한다.       <-  플레이어 의 인벤토리에 아이템이 전달되었다는 결과를 리턴한다.

 

이런식으로 동시에 데이터처리가 이루어질 수 있다.


대충 개념을 이해했으니, 이제 실제로 DeviceContext에 데이터를 넘기고, Collision처리를 한 다음에, 결과값을 리턴하고.

 

해당 리턴값을 사용해서 충돌했는지 확인한다음. 충돌위치를 출력해보자.

 

CPU에서 처리 동작

DeviceContext에 넘겨줄 삼각형 형태의 Face데이터를 배열로 저장하는것과 Buffer를 전달하는것,

그리고 리턴받은 Buffer를 분해해서 해당 배열에서 충돌여부와 충돌위치를 받아 출력하는것 이다.

 

GPU에서 처리할 동작

Device에서 넘겨받은 Face배열데이터를 순회하면서. 평면방정식을 사용해서 삼각형 Face에서 Ray가 충돌했는지, 여부와 충돌위치를 검사해서 배열로 제작한다. 그리고 Buffer로 Device에 전달한다.

 


우선 데이터를 넘겨주고, 정의해줄 Shader객체를 생성해주어야 한다.

#pragma once

class ComputeShader : public Shader
{
private:
	friend class Shader;

	ComputeShader(wstring file);
	~ComputeShader();

public:
	virtual void Set() override;

private:
	ID3D11ComputeShader* computeShader;
};
#include "Framework.h"

ComputeShader::ComputeShader(wstring file)
{
    DWORD flags = D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_DEBUG;
    //D3D_COMPILE_STANDARD_FILE_INCLUDE 쉐이더 로드용 헤더를 쓴다는뜻
    HRESULT result = D3DCompileFromFile(file.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE,
        "CS", "cs_5_0", flags, 0, &blob, nullptr);

    assert(SUCCEEDED(result));

    DEVICE->CreateComputeShader(blob->GetBufferPointer(), blob->GetBufferSize(), 
        nullptr, &computeShader);
}

ComputeShader::~ComputeShader()
{
    computeShader->Release();
}

void ComputeShader::Set()
{
    DC->CSSetShader(computeShader, nullptr, 0);
}

일반적인 Shader와 같은 방식으로 생성하나, 진입점 이름이 CS라는것과  CSSetShader로 데이터를 전달하는것 이 다르다.

 

Shader데이터를 넘기는것 과, 상수값으로 데이터를 buffer에 넘거야 함으로, 상수버퍼구조체에 Cs를 추가해준다.

#pragma once
class ConstBuffer
{
public:
	ConstBuffer(void* data, UINT dataSize);
	~ConstBuffer();

	void SetVS(UINT slot);
	void SetPS(UINT slot);
	void SetCS(UINT slot);

private:
	ID3D11Buffer* buffer = nullptr;

	void* data;
	UINT dataSize;

	D3D11_MAPPED_SUBRESOURCE subResouce;
};
#include "Framework.h"

ConstBuffer::ConstBuffer(void* data, UINT dataSize)
	: data(data), dataSize(dataSize)
{
    //ConstantBuffer
    D3D11_BUFFER_DESC bufferDesc = {};
    bufferDesc.Usage = D3D11_USAGE_DYNAMIC;
    bufferDesc.ByteWidth = dataSize;
    bufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    bufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;

    DEVICE->CreateBuffer(&bufferDesc, nullptr, &buffer);
}

ConstBuffer::~ConstBuffer()
{
    buffer->Release();
}

void ConstBuffer::SetVS(UINT slot)
{
    // 말이 상수버퍼이지 cpu에서 사용할때 에는 cpu에서 큐브 돌릴때의 월드행렬이 들어있다.
    //DC->UpdateSubresource(buffer, 0, nullptr, data, 0, 0);
    DC->Map(buffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &subResouce);
    // subResouce에 GPU주소값이 들어간다. 여기에 데이터를 밀어넣어줄 수 있다.
    memcpy(subResouce.pData, data, dataSize);
    DC->Unmap(buffer, 0);

    DC->VSSetConstantBuffers(slot, 1, &buffer); // slot = hlsl slot number
}

void ConstBuffer::SetPS(UINT slot)
{
    //DC->UpdateSubresource(buffer, 0, nullptr, data, 0, 0);
    DC->Map(buffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &subResouce);
    // subResouce에 GPU주소값이 들어간다. 여기에 데이터를 밀어넣어줄 수 있다.
    memcpy(subResouce.pData, data, dataSize);
    DC->Unmap(buffer, 0);

    DC->PSSetConstantBuffers(slot, 1, &buffer); // slot = hlsl slot number
}

void ConstBuffer::SetCS(UINT slot)
{
    //DC->UpdateSubresource(buffer, 0, nullptr, data, 0, 0);
    DC->Map(buffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &subResouce);
    // subResouce에 GPU주소값이 들어간다. 여기에 데이터를 밀어넣어줄 수 있다.
    memcpy(subResouce.pData, data, dataSize);
    DC->Unmap(buffer, 0);

    DC->CSSetConstantBuffers(slot, 1, &buffer); // slot = hlsl slot number
}

 


DC->Map

Rock이라는 개념이 있다. GPU에 접근하려면 메모리를 열어주어야 한다.

메모리를 열면, 렌더링을 못함. 열고 닫아야. 렌더링 작업을 진행할 수 있다.

 

Dc->UnMap

상수버퍼에서 보통 많이 사용한다.

 

자주 바뀌는 데이터는위의 형식을 사용하는게 좋다.

컴퓨트쉐이더는 만들때마다 버전과 진입점을 바꿔줘야한다.

 

컴퓨트쉐이더는 디스패치를 Draw대신 쓴다.


이제 함수가 DeviceContext에 데이터를 넘기고 받아와서 boolType으로 연산처리하는 과정을 살펴보자.

 

bool TerrainEditer::ComputePicking(Vector3& pos)
{
	// 마우스 위치로부터 레이 생성
	Ray ray = CAM->ScreenPointToRay(mousePos);

	// 레이와 삼각형의 교차 여부를 병렬처리하기 위해 필요한 데이터 설정
	rayBuffer->Get().pos = ray.pos;
	rayBuffer->Get().dir = ray.dir;
	rayBuffer->Get().triangleSize = triangleSize;

	// 레이 버퍼를 Compute Shader에 전달
	rayBuffer->SetCS(0);

	// 구조화된 버퍼를 Compute Shader에서 읽기 위한 SRV로 설정
	DC->CSSetShaderResources(0, 1, &structuredBuffer->GetSRV());

	// 구조화된 버퍼를 Compute Shader에서 쓰기 위한 UAV로 설정
	DC->CSSetUnorderedAccessViews(0, 1, &structuredBuffer->GetUAV(), nullptr);

	// Compute Shader를 설정
	computeShader->Set();

	// 쓰레드 그룹 수 계산 (올림 처리)
	UINT x = ceil((float)triangleSize / 64.0f);

	// Compute Shader 실행
	DC->Dispatch(x, 1, 1);
	//---------------------------------------
	// 이 이후에 outputs데이터에 데이터처리결과가 저장된다.


	// 결과를 CPU로 복사
	structuredBuffer->Copy(outputs.data(), sizeof(OutputDesc) * triangleSize);

	// 최소 거리와 해당 인덱스 초기화
	float minDistance = FLT_MAX;
	int minIndex = -1;

	// 결과를 분석하여 최소 거리를 찾음
	UINT index = 0;
	for (OutputDesc output : outputs)
	{
		if (output.picked)
		{
			if (minDistance > output.distance)
			{
				minDistance = output.distance;
				minIndex = index;
			}
		}
		index++;
	}

	// 최소 거리가 업데이트되었다면 교차 지점을 계산하고 true 반환
	if (minIndex >= 0)
	{
		pos = ray.pos + ray.dir * minDistance;
		return true;
	}

	// 교차 지점이 없을 경우 false 반환
	return false;
}

 

제공받은 모든 데이터에 접근하여

 

교차되었다면, 교차거리를 대입하고, 업데이트 되었을 때 교차지점 계산하고 true로 변환한다.

 

연산 자체는 CPU에서 이루어지지 않는 모습을 볼 수 있다.

 


멀티프로세스 방식을 사용하지 않았을 때,

프레임바가 최대 40대까지 떨어지는걸 볼 수 있다. 데이터가 return되는 부분이 뒤에있을수록 더 오래 연산하기 때문,

 

멀티프로세스를 사용했을 때

프레임도 600가까이 나오는걸 볼 수 있고, 뒤에 마우스를 가져다 대도 똑같은 속도가 보장되는것 을 볼 수 있다.

728x90