프로그래밍 공부
작성일
2023. 11. 28. 15:09
작성자
WDmil
728x90

Instancing

인스턴싱은, 최적화 기법중 하나로 동일 객체나 요소를 여러번 그리는 작업을 최적화 하기 위한 기술이다.

 

동일한 메시를 그릴 때 여러번 DrawCall 을 할 필요 없이 한번만 DrawCall 을 한 뒤, 같은 모델을 여러번 배치하는 형태로 이루어진다.

 

한번 생성된 데이터는 다시 날릴 필요 없이. 그 횟수동안 받아온 위치정보만 대입하여 그려주면 되기 때문에

 

여러번 데이터버퍼를 받아올 필요가 없기 때문이다.

 

이는 DrawCall 이 데이터를 Device에서 DeviceContext에서 넘기는 과정에서 생기는 Bus Latency나 Moemory Latency가 쌓이면 엄청난 시간적 손해가 발생하기 때문이다.

 

그래서 DrawCall 은 리소스를 많이 쓰는 작업이고 이러한 DrawCall을 줄이면 줄일수록 더 효율적인 프로그램이 된다.

 

인스턴싱은 이러한 DrawCall을 줄일 수 있는 원론적인 방법중 한가지 이다.

 

Instancing의 조건

인스턴싱이 아무리 효율적이라고 해도, 정의하고 생성하는데 아무조건이 없는것 이 아니다.

 

인스턴싱을 할 객체간 공유되는 데이터가 존재해야 Instancing이 가능하며, 이는 다음과 같다.

 

MeshData를 설정하고 정의하는 VertexData

MeshData를 설정하고 정의하는 indexData

 

DrawCall을 할 때 생성되는 srv데이터

 

위 3가지가 동일할 때 에만 인스턴싱을 할 수 있다.


기본원리

 

예를들어 VertexBuffer데이터에 다음과 같은 정보가 포함된다고 해보자.

Pos

Uv

Tan

Normal

 

우리는 이러한 데이터가 묶여있다고 가정할 때, 여기의 데이터중 Pos데이터만 다른것으로 바꾸면

 

같은 객체가 다른위치에 존재할 수 있다는걸 알 수 있다.

 

 

위와같은 형태의 데이터구조가 된다.

 

여기서 중간의 데이터는 Pos의 Matrix데이터가 된다.

 

정의된 데이터의 위치값을 Matrix데이터가 바꾸고 그 바꾼 위치값대로 다시 Light등 데이터를 재처리하게 된다.

 

즉, DataBuffer를 호출하거나 보내는 일 없이 DeviceContext에서 모든 데이터를 처리할 수 있다는 의미이다.


인스턴싱을 안썼을때의 경우

인스턴싱을 안쓰고 간단한 Quad를 1000번 호출하여 이미지를 생성해보자.

	quads.resize(COUNT);
	for (Quad*& quad : quads)
	{
		quad = new Quad();
		quad->GetMaterial()->SetDiffuseMap(L"Textures/Landscape/Box.png");

		Vector3 pos(MATH->Random(-10, 10), MATH->Random(-10, 10));
		quad->SetLocalPosition(pos);
		quad->UpdateWorld();
	}

프레임이 197정도 나온다.


인스턴싱을 썼을의 경우

위 1000개의 사각형에 인스턴싱을 적용해서 처리해보자.

 

//TextureInstancing
#include "../VertexHeader.hlsli"
#include "../PixelHeader.hlsli"

struct VertexInput
{
	float4 pos : POSITION;
	float2 uv : UV;
	
	matrix transform : INSTANCE_TRANSFORM;
};

struct PixelInput
{
	float4 pos : SV_POSITION;
	float2 uv : UV;
};

PixelInput VS(VertexInput input)
{
	PixelInput output;
	output.pos = mul(input.pos, input.transform);
	output.pos = mul(output.pos, view);
	output.pos = mul(output.pos, projection);
	
	output.uv = input.uv;
	
	return output;
}


float4 PS(PixelInput input) : SV_TARGET
{
	return diffuseMap.Sample(samp, input.uv) * mDiffuse;
}

우선 hlsl을 새로 만들어준다.

 

우리는 같은 Buffer를 사용해서 다른 Matrix를 적용해야 할것이다.

 

이때, Buffer로 받아올 VertexInput은 위치값인 transform을 위와같이 가져와서,

 

Outputpos를 transform으로 정의해서 사용해야 한다.

 

받아온 pos에 transform을 사용해서. 위치 Matrix로 위치를 바꾸어주는것 이다.

 

그리고 VertexBuffer에서 데이터를 넘겨줄 때 위치slot을 정의해서 1000개의 Matrix를 DeviceContext에 넘겨준다.

void VertexBuffer::Set(UINT slot, D3D11_PRIMITIVE_TOPOLOGY type)
{
    DC->IASetVertexBuffers(slot, 1, &buffer, &stride, &offset);
    DC->IASetPrimitiveTopology(type);
}

1번 버퍼에 넘겨줄것이다.

        temp = temp.substr(0, n);
        if (temp == "INSTANCE")
        {
            elementDesc.InputSlot = 1;
            elementDesc.InputSlotClass = D3D11_INPUT_PER_INSTANCE_DATA;
            elementDesc.InstanceDataStepRate = 1;
        }

vertexShader에서 데이터를 넘겨줄 때 이름이 INSTANCE일경우 슬롯을 1로 변환하고 INSTANCE_DATA라는것을 정의해준다.

 

그리고 RenderCall시, DrawInstanced라고 Instanced전용 데이터를 넘겨주고 RenderCall을 해주어야 한다.

inline void Mesh<T>::DrawInstanced(UINT instanceCount, D3D11_PRIMITIVE_TOPOLOGY type)
{
	vertexBuffer->Set(type);

	if (indexBuffer)
	{
		indexBuffer->Set();
		DC->DrawIndexedInstanced(indices.size(), instanceCount, 0, 0, 0);
	}
	else
	{
		DC->DrawInstanced(vertices.size(), instanceCount, 0, 0);
	}
}

Buffer에 Indstaced를 정의해주어, 해당 Buffer가 몃번 사용될것 인지 알려주어야 한다.

 

Draw될 객체가 정해진값 대로 다 사용되었으면 삭제해야 데이터가 절약된다.

InstancingScene::InstancingScene()
{
	//quads.resize(COUNT);
	//for (Quad*& quad : quads)
	//{
	//	quad = new Quad();
	//	quad->GetMaterial()->SetDiffuseMap(L"Textures/Landscape/Box.png");

	//	Vector3 pos(MATH->Random(-10, 10), MATH->Random(-10, 10));
	//	quad->SetLocalPosition(pos);
	//	quad->UpdateWorld();
	//}

	quad = new Quad();
	quad->GetMaterial()->SetDiffuseMap(L"Textures/Landscape/Box.png");
	quad->GetMaterial()->SetShader(L"Instancing/TextureInstancing.hlsl");

	instanceData.resize(COUNT);

	for (Matrix& transform : instanceData)
	{
		Vector3 pos(MATH->Random(-10, 10), MATH->Random(-10, 10));

		transform = XMMatrixTranslation(pos.x, pos.y, pos.z);

		transform = XMMatrixTranspose(transform); // 전치행렬화
	}

	instanceBuffer = new VertexBuffer(instanceData.data(), sizeof(Matrix), COUNT);

}

InstancingScene::~InstancingScene()
{
	delete quad;
	delete instanceBuffer;
}

void InstancingScene::Update()
{
}

void InstancingScene::PreRender()
{
}

void InstancingScene::Render()
{
	//for (Quad* quad : quads)
	//	quad->Render();

	instanceBuffer->Set(1);
	quad->RenderInstanced(COUNT);

}

void InstancingScene::PostRender()
{
}

void InstancingScene::GUIRender()
{
}

 

이제 이 인스턴싱된 객체를 출력해보자.

 

여기서 RenderInstaced가 행될때, COUNT값을 넘겨 1000번 사용될것 임을 명시해주고.

 

그 Matrix의 개수는 Registaer 1번에 준다.

FPS가 엄청 크게 뛴것을 볼 수 있다.


DirectX SkyBox

 

SkyBox란, 주변 환경을 맵으로 표현하기 위해 사용하는 방식이다.

 

두가지 방법이 있으며

 

맵 전체에 아주 크게 텍스쳐를 표현하는 방법 1.

카메라 위치에 동기화해서 VIewMatrix의 중앙위치값에 박아버리는 방법 2.

 

어차피 DirectX의 CamPos는 Cam이 움직이는것 이 아닌, Cam의 ViewMatrix에 따라 Object의 위치값과 Render여부가 결정되는것 이기 때문에,

 

ViewPort의 중심에 SkyBox를 위치시켜버리면 된다.

 

이도저도 아니라면, 그냥 맵 전체를 덮고도 남을정도로 매우 큰 구체를 만들고, 그 구체를 정중앙에 위치시키면 그것도 전체 배경처럼 나타난다.

 

구현해보자.

 


class SkyBox : public Sphere
{
public:
	SkyBox(wstring textureFile);
	~SkyBox();

	void Render();

private:
	Texture* cubeMap;

	RasterizerState* rasterizerSate[2];
	DepthStencilState* depthState[2];
};

 

SkyBox이다. 구체를 상속받으며, CubeMap형태의 .dds파일을 사용하며, 현재 Draw의 방향을 조절하기위한 ResterizerState와 깊이값을 조정하기위한 DepthStencilSate를 사용한다.

 

SkyBox::SkyBox(wstring textureFile)
{
	material->SetShader(L"Landscape/SkyBox.hlsl");

	cubeMap = Texture::Add(textureFile);

	rasterizerSate[0] = new RasterizerState();
	rasterizerSate[1] = new RasterizerState();
	rasterizerSate[1]->FrontCounterClockwise(true);

	depthState[0] = new DepthStencilState();
	depthState[1] = new DepthStencilState();
	depthState[1]->DepthEnable(false);
}

SkyBox::~SkyBox()
{
}

void SkyBox::Render()
{
	cubeMap->PSSet(10);
	rasterizerSate[1]->SetState();
	depthState[1]->SetState();

	Sphere::Render();
	rasterizerSate[0]->SetState();
	depthState[0]->SetState();

}

코드 자체는 매우 간단하다.

 

Texture데이터를 PiexelShader의 Register 10번에 넣어주고,

 

rasterizerState를 거꾸로 그려주고,

렌더한다음 다시 정방향으로 그려주면 된다.

그리고 해당 객체는 깊이검사를 하지않는다고 선언해주면 된다.

 

//Tutorial.hlsl
#include "../VertexHeader.hlsli"
#include "../PixelHeader.hlsli"

struct PixelInput
{
	float4 pos : SV_POSITION;
	float3 originPos : POSITION;
};

PixelInput VS(VertexUV input)
{
	PixelInput output;
	output.pos.xyz = mul(input.pos.xyz, (float3x3)view);
	output.pos.w = 1.0f;
	
	output.pos = mul(output.pos, projection);
	
	output.originPos = input.pos.xyz;
	
	return output;
}

TextureCube cubeMap : register(t10);

float4 PS(PixelInput input) : SV_TARGET
{
	return float4(cubeMap.Sample(samp, input.originPos).rgb, 1.0f);

}

hlsl에서는 originPos로 Position값을 받아온다.

 

originPos는 vertex값에 대해 viewMatrix를 곱해버린 다음 pos.w로 되어있는 거리값을 1로 초기화하고.

 

위치정보를 갱신해주면 된다.

 

dds파일은 2D텍스쳐에 3D입방체가 들어있음으로, 해당 위치값을 3차원 자표로 Sampling해준 뒤에 rgb값과 투명값을 넣어주면

 

위와같이 Float4형태로 해당 픽셀 위치를 만들어줄 수 있다.

 

깊이값은 정의되지 않음으로. 그 어떤것이라도 더 앞에 있다면 앞에 표시된다.

 

 

728x90