서의 공간

[T26] Dynamic Vertex 본문

Graphics API/DirectX 11 - Chili

[T26] Dynamic Vertex

홍서의 2020. 12. 5. 11:55

핵심 개념

1. c++ template meta programming

2. c++ reflection

3. std::move, std::forward

4. universal references

5. perfect forwarding

6. template recursion

7. T&&... args -> parameter pack 이라고 한다. 이것은 파라미터가 한개여도 되고 여러개여도 된다는 것이다.

8. 왜 이렇게 설계를 해야만 하는가?

현재 프로젝트에서 SkinnedBox를 만드는 과정을 살펴보면, Vertex 구조체를 정의한다. Vertex 구조체는 pos, normal, texturecoord 을 포함한다. geometry 클래스 중 하나인 cube 클래스에서 Make() 함수로 정점들의 구조를 결정하고, local space 기준 정점 좌표를 설정하고(uv좌표 포함), 모든 정점을 index화 한다. 문제는 cube 클래스에서 각 정점의 local 위치에 SkinnedBox 클래스에 있는 Vertex 구조체에 있는 변수명을 그대로 사용하여 데이터를 대입하는 과정에 있다. 만약 SkinnedBox 클래스의 Vertex 구조체에서 pos라는 변수가 없다면, Cube 클래스에서 Make()하는 과정에 컴파일 오류가 발생한다는 것이다. import 하려는 모든 외부 3D모델의 각 정점이 어떤 특성(Attribute)를 갖는지 항상 다 알 수 없다는 것이다. 또한 InputLayout, VertexShader에서 Vertex 구조체와 같은 구조가 중복(duplicate)된다는 문제도 있다. 런타임에 Vertex의 형식을 알아야 한다. 이것을 모두 자동화할 것인데, 이것을 dynamic vertex format system이라고 부른다. 런타임에 다이나믹하게 정점의 특성을 결정하기 위해 이 과정을 시작한다.

다음은 Vertex.h를 단순화 시킨 코드이다. 선언 부분만 알아보기 쉽게 정리했다. 각 함수와 변수가 무슨 역할을 하는지 집중적으로 살펴본다. 여담으로 GTA V 게임 엔진의 3D 애셋에 이 시스템을 사용한다는 글을 있다.

#pragma once
struct BGRAColor;

class VertexLayout
{
public:
	// 정점 구조체가 가질 수 있는 데이터 타입 집합.
	enum ElementType;

	/* Map이라는 구조체를 만든다. std::map과 헷갈리지 말 것.
	이 구조체는 template 특수화를 통해서 위 ElementType 집합안에 각 원소의
	여러 속성을 정의한다. 여러 속성은 DXGI_FORMAT과 셰이더에서 쓰일 semantic 이름이다.
	추가로 template 특수화에서 ElementType에 독립적으로 데이터 타입을 얻고자
	Map 구조체 안에서 ElementType에 대응하는 시스템 자료 타입을 
	using키워드를 사용하여 SysType으로 타입 에일리어스한다.*/
	template<ElementType>
	struct Map;

	/* Element 클래스는 enum ElementType에 몇 가지 기능과 속성을 추가한 클래스이다.
	쉽게 예를들면 Element의 생성자를 Position2D로 초기화하게 되면
	Position2D타입의 offset이라던지, type, InputLayout Desc을 얻을 수 있다.*/
	class Element
	{
	public:
		Element(ElementType type, size_t offset);
		// 데이터의 offset으로부터 자신의 크기만큼 떨어진 offset을 반환.
		size_t GetOffsetAfter() const;
		// 데이터 offset 반환.
		size_t GetOffset() const;
		// 데이터의 크기. 아래 Sizeof() 함수를 래핑함.
		size_t Size() const;
		// 파라미터로 주어진 type의 대응하는 시스템 타입의 크기를 반환.
		static constexpr size_t Sizeof(ElementType type);
		// 타입 반환.
		ElementType GetType() const;
		// 내부적으로 GenerateDesc() 함수를 호출. 반환 타입 확인.
		D3D11_INPUT_ELEMENT_DESC GetDesc() const;
	private:
		/* 위에서 Map구조체에 의해 semantic 이름과 DXGI_FORMAT이
		정의되었고 추가로 offset을 통해 Desc을 생성함. */
		template<ElementType type>
		static constexpr D3D11_INPUT_ELEMENT_DESC GenerateDesc(size_t offset);
	private:
		ElementType type;
		size_t offset;
	}
	// elements 배열에서 템플릿 파라미터 Type과 같은 Element를 찾아 반환.
	template<ElementType Type>
	const Element& Resolve() const;
	// 인덱스로 elements 배열에서 Element 찾음.
	const Element& ResolveByIndex(size_t i) const;
	/* elements 배열에 element 추가. 내부에서 type으로 element를 만듬.
	자기 자신을 참조로 리턴한다. */
	VertexLayout& Append(ElementType type);
	// elements의 전체 크기를 리턴
	size_t Size() const;
	// elements의 개수 리턴
	size_t GetElementCount() const;
	// 정점 속성의 개수에 맞추어 전체 DESC 완성하여 리턴함.
	vector<D3D11_INPUT_ELEMENT_DESC> GetD3DLayout() const;
private:
	vector<Element> elements;
}

class Vertex
{
	friend class VertexBuffer;
public:
	// 시스템 변수의 참조형을 리턴
	template<VertexLayout::ElementType Type>
	auto& Attr();
	// 내부적으로 val에 대응하여 private인 SetAttribute() 함수를 호출한다.
	template<typename T>
	void SetAttributeByIndex(size_t i, T&& val);
protected:
	// 생성자
	Vertex(char* pData, const VertexLayout& layout);
private:
	/*템플릿 재귀로 first만 떼서 계속 위에 public인 함수 SetAttributeByIndex()을 실행하고
	두번째 함수 콜은 first를 제외한 나머지 파라미터 팩으로 재귀호출*/ 
	template<typename First, typename ...Rest>
	void SetAttributeByIndex(size_t i, First&& first, Rest&&... rest);
	/* SetAttribute<VertexLayout::Position2D>(pAttribute, std::forward<T>(val));
	DestLayoutType이 Position2D이고 그것의 SysType을 Dest로 에일리어스
	is_assignable<To, From>은 From 타입을 To 타입에 할당할 수 있는지 여부를 테스트
	뒤에 value는 true아니면 false임
	그래서 true이면(파라미터로 주어진 val이 Dest에 할당가능하면)
	val을 pAttribute에 대입(동시에 형변환)
	fasle이면 assert*/
	template<VertexLayout::ElementType DestLayoutType, typename SrcType>
	void SetAttribute(char* pAttribute, SrcType&& val);
private:
	char* pData;
	VertexLayout& layout;
}

class ConstVertex
{
public:
	// 생성자
	ConstVertex(const Vertex& v);
	// 그냥 const_cast로 const떼서 vertex.Attr() 호출 함.
	template<VertexLayout::ElementType Type>
	const auto& Attr() const;
private:
	Vertex vertex;
}

class VertexBuffer
{
public:
	// 생성자
	VertexBuffer(VertexLayout layout);
	// char는 1바이트임. vector에 저장된 모든 data의 첫번째 원소 주소 반환
	const char* GetData() const;
	// VertexLayout 반환
	const VertexLayout& GetLayout() const;
	/* 버퍼의 크기 / layout의 크기
	버퍼는 데이터 타입 하나의 버퍼를 얘기하는 듯 싶다.
	버퍼의 size가 8이라면 8바이트이고, layout의 Size()는 SysType 타입의 사이즈이다.
	나누기 하면 해당 타입 데이터가 버퍼에 몇 개 들어있는지 알 수 있다.*/
	size_t Size() const;
	// 버퍼가 총 몇 바이트인지
	size_t SizeBytes() const;
	/* layout.GetElementCount()는 속성의 개수가 몇 개인지 그것이 params의 개수가 같아야 함
	그리고 버퍼를 늘리고, Back()함수를 호출한다.*/
	template<typename ...Params>
	void EmplaceBack(Params&&... params);
	/* buffer.data()는 배열의 첫번째 원소 주소를 반환 
	buffer.size()는 buffer가 몇 바이트인지
	layout.Size()는 속성의 사이즈(float3이면 12바이트) 이건 절대위치임.
	그래서 layout.Size()로 뺌으로서 빈 공간을 만들고 거기다 layout을 넣는 구조*/
	Vertex Back();
	Vertex Front();
	// 접근 연산 쉽게 하기 위해 연산자 오버로딩
	Vertex operator[](size_t i);
	ConstVertex Back() const;
	ConstVertex Front() const;
	ConstVertex operator[](size_t i) const;
private:
	vector<char> buffer;
	VertexLayout layout;
}

Vertex.h

더보기
#pragma once
#include <vector>
#include <type_traits>
#include "Graphics.h"

namespace hw3dexp
{
	struct BGRAColor
	{
		unsigned char a;
		unsigned char r;
		unsigned char g;
		unsigned char b;
	};

	class VertexLayout
	{
	public:
		enum ElementType
		{
			Position2D,
			Position3D,
			Texture2D,
			Normal,
			Float3Color,
			Float4Color,
			BGRAColor,
			Count,
		};
		template<ElementType> struct Map;
		template<> struct Map<Position2D>
		{
			using SysType = DirectX::XMFLOAT2;
			static constexpr DXGI_FORMAT dxgiFormat = DXGI_FORMAT_R32G32_FLOAT;
			static constexpr const char* semantic = "Position";
		};
		template<> struct Map<Position3D>
		{
			using SysType = DirectX::XMFLOAT3;
			static constexpr DXGI_FORMAT dxgiFormat = DXGI_FORMAT_R32G32B32_FLOAT;
			static constexpr const char* semantic = "Position";
		};
		template<> struct Map<Texture2D>
		{
			using SysType = DirectX::XMFLOAT2;
			static constexpr DXGI_FORMAT dxgiFormat = DXGI_FORMAT_R32G32_FLOAT;
			static constexpr const char* semantic = "Texcoord";
		};
		template<> struct Map<Normal>
		{
			using SysType = DirectX::XMFLOAT3;
			static constexpr DXGI_FORMAT dxgiFormat = DXGI_FORMAT_R32G32B32_FLOAT;
			static constexpr const char* semantic = "Normal";
		};
		template<> struct Map<Float3Color>
		{
			using SysType = DirectX::XMFLOAT3;
			static constexpr DXGI_FORMAT dxgiFormat = DXGI_FORMAT_R32G32B32_FLOAT;
			static constexpr const char* semantic = "Color";
		};
		template<> struct Map<Float4Color>
		{
			using SysType = DirectX::XMFLOAT4;
			static constexpr DXGI_FORMAT dxgiFormat = DXGI_FORMAT_R32G32B32A32_FLOAT;
			static constexpr const char* semantic = "Color";
		};
		template<> struct Map<BGRAColor>
		{
			using SysType = hw3dexp::BGRAColor;
			static constexpr DXGI_FORMAT dxgiFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
			static constexpr const char* semantic = "Color";
		};

		class Element
		{
		public:
			Element(ElementType type, size_t offset)
				:
				type(type),
				offset(offset)
			{}
			size_t GetOffsetAfter() const noexcept(!IS_DEBUG)
			{
				return offset + Size();
			}
			size_t GetOffset() const
			{
				return offset;
			}
			size_t Size() const noexcept(!IS_DEBUG)
			{
				return SizeOf(type);
			}
			static constexpr size_t SizeOf(ElementType type) noexcept(!IS_DEBUG)
			{
				switch (type)
				{
				case Position2D:
					return sizeof( Map<Position2D>::SysType);
				case Position3D:
					return sizeof(Map<Position3D>::SysType);
				case Texture2D:
					return sizeof(Map<Texture2D>::SysType);
				case Normal:
					return sizeof(Map<Normal>::SysType);
				case Float3Color:
					return sizeof(Map<Float3Color>::SysType);
				case Float4Color:
					return sizeof(Map<Float4Color>::SysType);
				case BGRAColor:
					return sizeof(Map<BGRAColor>::SysType);
				}
				assert("Invalid element type" && false);
				return 0u;
			}
			ElementType GetType() const noexcept
			{
				return type;
			}
			D3D11_INPUT_ELEMENT_DESC GetDesc() const noexcept(!IS_DEBUG)
			{
				switch (type)
				{
				case Position2D:
					return GenerateDesc<Position2D>(GetOffset());
				case Position3D:
					return GenerateDesc<Position3D>(GetOffset());
				case Texture2D:
					return GenerateDesc<Texture2D>(GetOffset());
				case Normal:
					return GenerateDesc<Normal>(GetOffset());
				case Float3Color:
					return GenerateDesc<Float3Color>(GetOffset());
				case Float4Color:
					return GenerateDesc<Float4Color>(GetOffset());
				case BGRAColor:
					return GenerateDesc<BGRAColor>(GetOffset());
				}
				assert("Invalid element type" && false);
				return { "INVALID", 0, DXGI_FORMAT_UNKNOWN, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 };
			}
		private:
			template<ElementType type>
			static constexpr D3D11_INPUT_ELEMENT_DESC GenerateDesc(size_t offset) noexcept(!IS_DEBUG)
			{
				return { Map<type>::semantic, 0, Map<type>::dxgiFormat, 0, (UINT)offset, D3D11_INPUT_PER_VERTEX_DATA, 0 };
			}
		private:
			ElementType type;
			size_t offset;
		};
	public:
		template<ElementType Type>
		const Element& Resolve() const noexcept(!IS_DEBUG)
		{
			for (auto& e : elements)
			{
				if (e.GetType() == Type)
				{
					return e;
				}
			}
			assert("Could not resolve element type" && false);
			return elements.front();
		}
		const Element& ResolveByIndex(size_t i) const noexcept(!IS_DEBUG)
		{
			return elements[i];
		}
		VertexLayout& Append( ElementType type) noexcept(!IS_DEBUG)
		{
			elements.emplace_back(type, Size());
			return *this;
		}
		size_t Size() const noexcept(!IS_DEBUG)
		{
			return elements.empty() ? 0u : elements.back().GetOffsetAfter();
		}
		size_t GetElementCount() const noexcept
		{
			return elements.size();
		}
		std::vector<D3D11_INPUT_ELEMENT_DESC> GetD3DLayout() const noexcept(!IS_DEBUG)
		{
			std::vector<D3D11_INPUT_ELEMENT_DESC> desc;
			desc.reserve(GetElementCount());
			for (const auto& e : elements)
			{
				desc.push_back(e.GetDesc());
			}
			return desc;
		}
	private:
		std::vector<Element> elements;
	};

	class Vertex
	{
		friend class VertexBuffer;
	public:
		template<VertexLayout::ElementType Type>
		auto& Attr() noexcept(!IS_DEBUG)
		{
			auto pAttribute = pData + layout.Resolve<Type>().GetOffset();
			return *reinterpret_cast<typename VertexLayout::Map<Type>::SysType*>(pAttribute);
		}
		template<typename T>
		void SetAttributeByIndex(size_t i, T&& val) noexcept(!IS_DEBUG)
		{
			using namespace DirectX;
			const auto& element = layout.ResolveByIndex(i);
			auto pAttribute = pData + element.GetOffset();
			switch (element.GetType())
			{
			case VertexLayout::Position2D:
				SetAttribute<VertexLayout::Position2D>(pAttribute, std::forward<T>(val));
				break;
			case VertexLayout::Position3D:
				SetAttribute<VertexLayout::Position3D>(pAttribute, std::forward<T>(val));
				break;
			case VertexLayout::Texture2D:
				SetAttribute<VertexLayout::Texture2D>(pAttribute, std::forward<T>(val));
				break;
			case VertexLayout::Normal:
				SetAttribute<VertexLayout::Normal>(pAttribute, std::forward<T>(val));
				break;
			case VertexLayout::Float3Color:
				SetAttribute<VertexLayout::Float3Color>(pAttribute, std::forward<T>(val));
				break;
			case VertexLayout::Float4Color:
				SetAttribute<VertexLayout::Float4Color>(pAttribute, std::forward<T>(val));
				break;
			case VertexLayout::BGRAColor:
				SetAttribute<VertexLayout::BGRAColor>(pAttribute, std::forward<T>(val));
				break;
			default:
				assert("Bad element type" && false);
			}
		}
	protected:
		Vertex(char* pData, const VertexLayout& layout) noexcept(!IS_DEBUG)
			:
			pData(pData),
			layout(layout)
		{
			assert(pData != nullptr);
		}
	private:
		template<typename First, typename ...Rest>
		// enables paramter pack setting of multiple parameters by element index
		void SetAttributeByIndex(size_t i, First&& first, Rest&&... rest) noexcept(!IS_DEBUG)
		{
			SetAttributeByIndex(i, std::forward<First>(first));
			SetAttributeByIndex(i + 1, std::forward<Rest>(rest)...);
		}
		// helper to reduce code duplication in SetAttributeByIndex
		template<VertexLayout::ElementType DestLayoutType, typename SrcType>
		void SetAttribute(char* pAttribute, SrcType&& val) noexcept(!IS_DEBUG)
		{
			using Dest = typename VertexLayout::Map<DestLayoutType>::SysType;
			if constexpr (std::is_assignable<Dest, SrcType>::value)
			{
				*reinterpret_cast<Dest*>(pAttribute) = val;
			}
			else
			{
				assert("Parameter attribute type mismatch" && false);
			}
		}
	private:
		char* pData = nullptr;
		const VertexLayout& layout;
	};

	class ConstVertex
	{
	public:
		ConstVertex(const Vertex& v) noexcept(!IS_DEBUG)
			:
			vertex(v)
		{}
		template<VertexLayout::ElementType Type>
		const auto& Attr() const noexcept(!IS_DEBUG)
		{
			return const_cast<Vertex&>(vertex).Attr<Type>();
		}
	private:
		Vertex vertex;
	};

	class VertexBuffer
	{
	public:
		VertexBuffer(VertexLayout layout) noexcept(!IS_DEBUG)
			:
			layout(std::move(layout))
		{}
		const char* GetData() const noexcept(!IS_DEBUG)
		{
			return buffer.data();
		}
		const VertexLayout& GetLayout() const noexcept
		{
			return layout;
		}
		size_t Size() const noexcept(!IS_DEBUG)
		{
			return buffer.size() / layout.Size();
		}
		size_t SizeBytes() const noexcept(!IS_DEBUG)
		{
			return buffer.size();
		}
		template<typename ...Params>
		void EmplaceBack(Params&&... params) noexcept(!IS_DEBUG)
		{
			assert(sizeof...(params) == layout.GetElementCount() && "Param count doesn't match number of vertex elements");
			buffer.resize(buffer.size() + layout.Size());
			Back().SetAttributeByIndex(0u, std::forward<Params>(params)...);
		}
		Vertex Back() noexcept(!IS_DEBUG)
		{
			assert(buffer.size() != 0u);
			return Vertex{ buffer.data() + buffer.size() - layout.Size(), layout };
		}
		Vertex Front() noexcept(!IS_DEBUG)
		{
			assert(buffer.size() != 0u);
			return Vertex{ buffer.data(), layout };
		}
		Vertex operator[](size_t i) noexcept(!IS_DEBUG)
		{
			assert(i < Size());
			return Vertex{ buffer.data() + layout.Size() * i, layout };
		}
		ConstVertex Back() const noexcept(!IS_DEBUG)
		{
			return const_cast<VertexBuffer*>(this)->Back();
		}
		ConstVertex Front() const noexcept(!IS_DEBUG)
		{
			return const_cast<VertexBuffer*>(this)->Front();
		}
		ConstVertex operator[](size_t i) const noexcept(!IS_DEBUG)
		{
			return const_cast<VertexBuffer&>(*this)[i];
		}
	private:
		std::vector<char> buffer;
		VertexLayout layout;
	};
}

VertexBuffer.h

더보기

두 번째 생성자의 두 번째 파라미터 const hw3dexp::VertexBuffer& vbuf는 Vertex.h에서 구현된 VertexBuffer임

#pragma once
#include "Bindable.h"
#include "GraphicsThrowMacros.h"
#include "Vertex.h"

class VertexBuffer : public Bindable
{
public:
	template<class V>
	VertexBuffer(Graphics& gfx, const std::vector<V>& vertices)
		:
		stride(sizeof(V))
	{
		INFOMAN(gfx);

		D3D11_BUFFER_DESC bd = {};
		bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
		bd.Usage = D3D11_USAGE_DEFAULT;
		bd.CPUAccessFlags = 0u;
		bd.MiscFlags = 0u;
		bd.ByteWidth = UINT(sizeof(V) * vertices.size());
		bd.StructureByteStride = sizeof(V);
		D3D11_SUBRESOURCE_DATA sd = {};
		sd.pSysMem = vertices.data();
		GFX_THROW_INFO(GetDevice(gfx)->CreateBuffer(&bd, &sd, &pVertexBuffer));
	}
	VertexBuffer(Graphics& gfx, const hw3dexp::VertexBuffer& vbuf)
		:
		stride((UINT)vbuf.GetLayout().Size())
	{
		INFOMAN(gfx);

		D3D11_BUFFER_DESC bd = {};
		bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
		bd.Usage = D3D11_USAGE_DEFAULT;
		bd.CPUAccessFlags = 0u;
		bd.MiscFlags = 0u;
		bd.ByteWidth = UINT(vbuf.SizeBytes());
		bd.StructureByteStride = stride;
		D3D11_SUBRESOURCE_DATA sd = {};
		sd.pSysMem = vbuf.GetData();
		GFX_THROW_INFO(GetDevice(gfx)->CreateBuffer(&bd, &sd, &pVertexBuffer));
	}
	void Bind(Graphics& gfx) noexcept override;
protected:
	UINT stride;
	Microsoft::WRL::ComPtr<ID3D11Buffer> pVertexBuffer;
};

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

[T34.1] Normal Mapping  (0) 2020.12.15
[T28] Model node tree  (0) 2020.12.07
[T25] Assimp 라이브러리  (0) 2020.12.05
[T24.2] Lighting  (0) 2020.12.05
[T24.1] Lighting  (0) 2020.12.05
Comments