서의 공간

[T21.2] Drawble System 본문

Graphics API/DirectX 11 - Chili

[T21.2] Drawble System

홍서의 2020. 11. 30. 18:48

다음은 도형을 그리기 위한 클래스이다. 기하구조를 결정하는 클래스까지. 

그리고 상기할 것은 투영 공간 변환 매트릭스를 구하고 이것이 각 정점에 곱하여 연산하는 것은 VertexShader의 역할이다. 우리는 단지 이 매트릭스를 셰이더에 제출한다. 물론 셰이더 파일에 각 정점과 이 매트릭스를 곱하라는 연산을 구현하긴 하지만 말이다.


Drawable 클래스

더보기

핵심 개념

Drawable는 모든 그리기 가능한 오브젝트의 베이스 클래스이다. 추상 클래스로서 DrawableBase가 이 클래스를 상속받고, 실제 오브젝트들은(Box와 같은) DrawableBase 클래스를 상속받는다. 함수의 역할을 하나씩 살펴본다.

GetTransformXM(): 이 함수는 오브젝트의 월드 공간 기준 매트릭스를 반환하는 함수이다.

Draw(): binds 배열에 저장되어 있는 모든 Bindable 인스턴스들을 bind 하고, staticBinds 배열에 저장되어 있는 모든 Bindable 인스턴스들을 파이프라인에 바인드 한다. staticBinds는 하위 클래스인 DrawableBase 클래스에서 정의한다.

Update(): 파라미터는 시간이고, 시간에 따른 변화를 업데이트한다.

AddBind(): Bindable 인스턴스를 binds 배열에 추가한다. 단, 인덱스 버퍼는 AddIndexBuffer() 함수를 사용한다.

AddIndexBuffer(): IndexBuffer 인스턴스를 binds 배열에 추가한다.

GetStaticBinds(): DrawableBase 클래스에서 구현하며, 이 함수는 staticBinds 배열에 Bindable 인스턴스를 저장한다.

 

이렇게 설계한 이유는 하나의 오브젝트 예를 들어 Box를 여러 개 그린다고 한다면, 또한 이 Box의 크기와 색상이 모두 다르다고 할 때, Box의 공통적인 부분들은 하나로 묶어야 할 필요가 있다. 예를 들어 Box를 그리기 위한 정점 이 8개라고 했을 때, 이 정점들의 상대적 위치와 기하구조는 Box를 아무리 많이 그려야 한다고 해도 달라지지 않는다. 따라서 VertexBuffer, VertexShader, PixelShader, IndexBuffer, PixelConstantBuffer, InputLayout, Topology 이 7개는 결국 Box를 그리기 위해서 공통적인 부분으로 한 번만 파이프라인에 바인드 되면 될 것이다. 따라서 이 Bindable 인스턴스들은 전부 staticBinds라는 배열에 저장하게 된다. 추가로 IndexBuffer는 그리기 함수 호출을 할 때 필요하므로 DrawablaBase 클래스의 SetIndexFromStatic() 함수에서 staticBinds 배열을 뒤져서 IndexBuffer를 찾은 후 따로 변수에 저장하여 나중에 사용한다. transformCbuf와 같은 매 프레임 투영 매트릭스를 구하고 이것을 상수 버퍼에 써야 하는 경우는 binds 배열에 저장하게 된다.   

 

Drawable.h

#pragma once
#include "Graphics.h"
#include <DirectXMath.h>

class Bindable;

class Drawable
{
	template<class T>
	friend class DrawableBase;
public:
	Drawable() = default;
	Drawable( const Drawable& ) = delete;
	virtual DirectX::XMMATRIX GetTransformXM() const noexcept = 0;
	void Draw( Graphics& gfx ) const noexcept(!IS_DEBUG);
	virtual void Update( float dt ) noexcept = 0;
	virtual ~Drawable() = default;
protected:
	void AddBind( std::unique_ptr<Bindable> bind ) noexcept(!IS_DEBUG);
	void AddIndexBuffer( std::unique_ptr<class IndexBuffer> ibuf ) noexcept(!IS_DEBUG);
private:
	virtual const std::vector<std::unique_ptr<Bindable>>& GetStaticBinds() const noexcept = 0;
private:
	const class IndexBuffer* pIndexBuffer = nullptr;
	std::vector<std::unique_ptr<Bindable>> binds;
};

Drawable.cpp

#include "Drawable.h"
#include "GraphicsThrowMacros.h"
#include "IndexBuffer.h"
#include <cassert>
#include <typeinfo>

void Drawable::Draw( Graphics& gfx ) const noexcept(!IS_DEBUG)
{
	for( auto& b : binds )
	{
		b->Bind( gfx );
	}
	for( auto& b : GetStaticBinds() )
	{
		b->Bind( gfx );
	}
	gfx.DrawIndexed( pIndexBuffer->GetCount() );
}

void Drawable::AddBind( std::unique_ptr<Bindable> bind ) noexcept(!IS_DEBUG)
{
	assert( "*Must* use AddIndexBuffer to bind index buffer" && typeid(*bind) != typeid(IndexBuffer) );
	binds.push_back( std::move( bind ) );
}

void Drawable::AddIndexBuffer( std::unique_ptr<IndexBuffer> ibuf ) noexcept(!IS_DEBUG)
{
	assert( "Attempting to add index buffer a second time" && pIndexBuffer == nullptr );
	pIndexBuffer = ibuf.get();
	binds.push_back( std::move( ibuf ) );
}

DrawableBase 클래스

더보기

DrawableBase.h

#pragma once
#include "Drawable.h"
#include "IndexBuffer.h"

template<class T>
class DrawableBase : public Drawable
{
protected:
	static bool IsStaticInitialized() noexcept
	{
		return !staticBinds.empty();
	}
	static void AddStaticBind( std::unique_ptr<Bindable> bind ) noexcept(!IS_DEBUG)
	{
		assert( "*Must* use AddStaticIndexBuffer to bind index buffer" && typeid(*bind) != typeid(IndexBuffer) );
		staticBinds.push_back( std::move( bind ) );
	}
	void AddStaticIndexBuffer( std::unique_ptr<IndexBuffer> ibuf ) noexcept(!IS_DEBUG)
	{
		assert( "Attempting to add index buffer a second time" && pIndexBuffer == nullptr );
		pIndexBuffer = ibuf.get();
		staticBinds.push_back( std::move( ibuf ) );
	}
	void SetIndexFromStatic() noexcept(!IS_DEBUG)
	{
		assert( "Attempting to add index buffer a second time" && pIndexBuffer == nullptr );
		for( const auto& b : staticBinds )
		{
			if( const auto p = dynamic_cast<IndexBuffer*>(b.get()) )
			{
				pIndexBuffer = p;
				return;
			}
		}
		assert( "Failed to find index buffer in static binds" && pIndexBuffer != nullptr );
	}
private:
	const std::vector<std::unique_ptr<Bindable>>& GetStaticBinds() const noexcept override
	{
		return staticBinds;
	}
private:
	static std::vector<std::unique_ptr<Bindable>> staticBinds;
};

template<class T>
std::vector<std::unique_ptr<Bindable>> DrawableBase<T>::staticBinds;

IndexedTriangleList.h

더보기

핵심 개념

정점의 로컬 공간 기준 정보와 정점의 인덱스 리스트를 반환해주는 클래스이다. 그냥 서로 다른 타입인 정점들과 인덱스 리스트를 한 곳에 모아주는 구조체로 생각하면 된다. Transform 함수는 파라미터가 변환 매트릭스이고(일반적으로 월드 변환 중 스케일 변환), 각 모든 정점에 대해 스케일 변환한다.

#pragma once
#include <vector>
#include <DirectXMath.h>

template<class T>
class IndexedTriangleList
{
public:
	IndexedTriangleList() = default;
	IndexedTriangleList( std::vector<T> verts_in,std::vector<unsigned short> indices_in )
		:
		vertices( std::move( verts_in ) ),
		indices( std::move( indices_in ) )
	{
		assert( vertices.size() > 2 );
		assert( indices.size() % 3 == 0 );
	}
	void Transform( DirectX::FXMMATRIX matrix )
	{
		for( auto& v : vertices )
		{
			const DirectX::XMVECTOR pos = DirectX::XMLoadFloat3( &v.pos );
			DirectX::XMStoreFloat3(
				&v.pos,
				DirectX::XMVector3Transform( pos,matrix )
			);
		}
	}

public:
	std::vector<T> vertices;
	std::vector<unsigned short> indices;
};

Cube.h

더보기

핵심 개념

큐브 기하구조를 만든다.

Make() 함수는 각 오브젝트에 정의 된 Vertex의 로컬 위치를 설정한다. 즉 vertex들이 상대적으로 어떻게 위치하는지 기하구조를 만든다. 중요한 것은 이 함수를 부르는 오브젝트에서 정의된 Vertex 구조체는 항상 pos라는 이름의 위치정보를 가져야 한다. 이 부분은 나중에 한 층 더 일반화할 것이다. 정점들의 로컬 좌표를 결정하고, 정점들을 인덱스화 시킨다. 그리고 이 두 가지(정점들, 인덱스 리스트)를 IndexedTriangleList로서 반환한다.

#pragma once
#include "IndexedTriangleList.h"
#include <DirectXMath.h>

class Cube
{
public:
	template<class V>
	static IndexedTriangleList<V> Make()
	{
		namespace dx = DirectX;

		constexpr float side = 1.0f / 2.0f;

		std::vector<dx::XMFLOAT3> vertices;
		vertices.emplace_back( -side,-side,-side ); // 0
		vertices.emplace_back( side,-side,-side ); // 1
		vertices.emplace_back( -side,side,-side ); // 2
		vertices.emplace_back( side,side,-side ); // 3
		vertices.emplace_back( -side,-side,side ); // 4
		vertices.emplace_back( side,-side,side ); // 5
		vertices.emplace_back( -side,side,side ); // 6
		vertices.emplace_back( side,side,side ); // 7

		std::vector<V> verts( vertices.size() );
		for( size_t i = 0; i < vertices.size(); i++ )
		{
			verts[i].pos = vertices[i];
		}
		return{
			std::move( verts ),{
				0,2,1, 2,3,1,
				1,3,5, 3,7,5,
				2,6,3, 3,6,7,
				4,5,7, 4,7,6,
				0,4,2, 2,4,6,
				0,1,4, 1,5,4
			}
		};
	}
};

Box 클래스

더보기

핵심 개념

드디어 Box를 그린다.

Box는 기본생성자의 파라미터 중 랜덤 값 5개를 받는데, 각 클래스 변수들을 설정한다. 

float r;                // 월드 공간 원점으로부터의 x축 거리
float roll = 0.0f;    // 로컬 공간 z축 회전
float pitch = 0.0f;  // 로컬 공간 x축 회전
float yaw = 0.0f;   // 로컬 공간 y축 회전
float theta;          // 월드 공간 기준 y축 회전
float phi;             // 월드 공간 기준 x축 회전
float chi;             // 월드 공간 기준 z축 회전

생성자의 마지막 줄은 각 모델의 인스턴스마다 z축 비례를 달리 두기 위해서이다. 랜덤 값을 받는다.

GetTransformXM() 함수에서 월드 공간 변환 매트릭스를 계산하고 반환한다. 이것은 transformCbuf 클래스에서 쓰이고, 최종적으로 VertexShader에 상수버퍼의 매트릭스로서 제출한다.

 

Box.h

#pragma once
#include "DrawableBase.h"

class Box : public DrawableBase<Box>
{
public:
	Box( Graphics& gfx,std::mt19937& rng,
		std::uniform_real_distribution<float>& adist,
		std::uniform_real_distribution<float>& ddist,
		std::uniform_real_distribution<float>& odist,
		std::uniform_real_distribution<float>& rdist,
		std::uniform_real_distribution<float>& bdist );
	void Update( float dt ) noexcept override;
	DirectX::XMMATRIX GetTransformXM() const noexcept override;
private:
	// positional
	float r;
	float roll = 0.0f;
	float pitch = 0.0f;
	float yaw = 0.0f;
	float theta;
	float phi;
	float chi;
	// speed (delta/s)
	float droll;
	float dpitch;
	float dyaw;
	float dtheta;
	float dphi;
	float dchi;
	// model transform
	DirectX::XMFLOAT3X3 mt;
};

Box.cpp

#include "Box.h"
#include "BindableBase.h"
#include "GraphicsThrowMacros.h"
#include "Cube.h"


Box::Box( Graphics& gfx,
	std::mt19937& rng,
	std::uniform_real_distribution<float>& adist,
	std::uniform_real_distribution<float>& ddist,
	std::uniform_real_distribution<float>& odist,
	std::uniform_real_distribution<float>& rdist,
	std::uniform_real_distribution<float>& bdist )
	:
	r( rdist( rng ) ),
	droll( ddist( rng ) ),
	dpitch( ddist( rng ) ),
	dyaw( ddist( rng ) ),
	dphi( odist( rng ) ),
	dtheta( odist( rng ) ),
	dchi( odist( rng ) ),
	chi( adist( rng ) ),
	theta( adist( rng ) ),
	phi( adist( rng ) )
{
	namespace dx = DirectX;

	if( !IsStaticInitialized() )
	{
		struct Vertex
		{
			dx::XMFLOAT3 pos;
		};
		const auto model = Cube::Make<Vertex>();

		AddStaticBind( std::make_unique<VertexBuffer>( gfx,model.vertices ) );

		auto pvs = std::make_unique<VertexShader>( gfx,L"ColorIndexVS.cso" );
		auto pvsbc = pvs->GetBytecode();
		AddStaticBind( std::move( pvs ) );

		AddStaticBind( std::make_unique<PixelShader>( gfx,L"ColorIndexPS.cso" ) );

		AddStaticIndexBuffer( std::make_unique<IndexBuffer>( gfx,model.indices ) );

		struct PixelShaderConstants
		{
			struct
			{
				float r;
				float g;
				float b;
				float a;
			} face_colors[8];
		};
		const PixelShaderConstants cb2 =
		{
			{
				{ 1.0f,1.0f,1.0f },
				{ 1.0f,0.0f,0.0f },
				{ 0.0f,1.0f,0.0f },
				{ 1.0f,1.0f,0.0f },
				{ 0.0f,0.0f,1.0f },
				{ 1.0f,0.0f,1.0f },
				{ 0.0f,1.0f,1.0f },
				{ 0.0f,0.0f,0.0f },
			}
		};
		AddStaticBind( std::make_unique<PixelConstantBuffer<PixelShaderConstants>>( gfx,cb2 ) );

		const std::vector<D3D11_INPUT_ELEMENT_DESC> ied =
		{
			{ "Position",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0 },
		};
		AddStaticBind( std::make_unique<InputLayout>( gfx,ied,pvsbc ) );

		AddStaticBind( std::make_unique<Topology>( gfx,D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST ) );
	}
	else
	{
		SetIndexFromStatic();
	}

	AddBind( std::make_unique<TransformCbuf>( gfx,*this ) );
	
	// model deformation transform (per instance, not stored as bind)
	dx::XMStoreFloat3x3(
		&mt,
		dx::XMMatrixScaling( 1.0f,1.0f,bdist( rng ) )
	);
}

void Box::Update( float dt ) noexcept
{
	roll += droll * dt;
	pitch += dpitch * dt;
	yaw += dyaw * dt;
	theta += dtheta * dt;
	phi += dphi * dt;
	chi += dchi * dt;
}

DirectX::XMMATRIX Box::GetTransformXM() const noexcept
{
	namespace dx = DirectX;
	return dx::XMLoadFloat3x3( &mt ) *
		dx::XMMatrixRotationRollPitchYaw( pitch,yaw,roll ) *
		dx::XMMatrixTranslation( r,0.0f,0.0f ) *
		dx::XMMatrixRotationRollPitchYaw( theta,phi,chi ) *
		dx::XMMatrixTranslation( 0.0f,0.0f,20.0f );
}

Shader

더보기

ColorIndexVS.hlsl

cbuffer CBuf
{
	matrix transform;
};

float4 main( float3 pos : Position ) : SV_Position
{
	return mul( float4(pos,1.0f),transform );
}

ColorIndexPS.hlsl

핵심 개념

SV_PrimitiveID는 기본도형(삼각형)마다 하나의 ID가 주어진다. 따라서 사각형의 한 면(삼각형 두 개)마다 하나의 색상을 칠하고 반환한다. face_colors 자체가 색상 데이터이다.

cbuffer CBuf
{
	float4 face_colors[8];
};

float4 main( uint tid : SV_PrimitiveID ) : SV_Target
{
	return face_colors[(tid/2) % 8];
}

다음은 Box를 100개 그린 결과이다.

[그림 21.2.1 Box 그리기]

'Graphics API > DirectX 11 - Chili' 카테고리의 다른 글

[T24.0] Lighting  (0) 2020.12.05
[T21.3] Box, Hill, Sphere, Cylinder, Skull, Wave  (1) 2020.12.01
[T21.1] Bindable System  (0) 2020.11.29
[T20] Solid cube, Depth and stencil  (0) 2020.11.29
[T18] Constant buffer  (0) 2020.11.29
Comments