0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DirectX12でミクさんを躍らせてみよう18-GPUインスタンシング

Last updated at Posted at 2024-10-01

前回

GPUインスタンシング

こんにちは。

今回は、初音ミクさんの前で多くのペンライトを描画できるようにしてみましょう。

これまでの方法では、1000本のペンライトを描くには1000回の描画命令が必要でした。

そして、ペンライトの位置も異なるため、毎回ワールドトランスフォームに関する定数バッファも変更しなければなりませんでした。

GPUインスタンシングを使用すると、より効率的にレンダリングが可能になります。

GPUインスタンシング

GPUインスタンシングとは、基本メッシュデータを一度だけ渡して、各インスタンスの個別の属性を渡すことで、GPUで個別のインスタンスをレンダリングすることを指します。

簡単に言えば、同じ頂点とインデックスを使用する場合、一度だけ設定し、各インスタンスの位置情報などのデータだけを別々に渡すことで、一度に複数のオブジェクトを描画できるようにすることです。

1000個を描画するために1000回の描画命令を行うのではなく、1回の命令だけで描画することができます。

その分だけドローコールが減少するため、パフォーマンスの向上が期待できます。

欠点は同じメッシュを複数描画するためのものであるため、インスタンスごとの形状を変えることが難しく、GPU性能に依存的です。処理しようとするインスタンスが多すぎる場合、GPUが追いつかないと効率が低下する可能性があります。

現在の私の環境はデスクトップ環境で、グラフィックカードも比較的新しい製品なので、GPU性能については心配する必要はなさそうです。

まず、GPUインスタンシングを実装する前にペンライトのメッシュを定義します。

ペンライトには細長い箱型のメッシュを使用する予定です。

このような単純なメッシュはFBXのようなファイルから読み込む必要はなく、直接定義して使うことにします。

ボックスメッシュ

class CubeGeometry : public IGeometry
{
public:
    CubeGeometry()
    {
        CreateGeometry();
    }

	virtual const std::vector<Vertex>& GetVertices() const override;
	virtual const std::vector<unsigned int>& GetIndices() const override;

private:
	void CreateGeometry();

private:
	std::vector<Vertex> mVertices{};
	std::vector<unsigned int> mIndices{};
};

inline const std::vector<Vertex>& CubeGeometry::GetVertices() const
{
    return mVertices;
}

inline const std::vector<unsigned>& CubeGeometry::GetIndices() const
{
    return mIndices;
}

inline void CubeGeometry::CreateGeometry()
{
    mVertices = {
        // Front face
        { { -0.5f, -0.5f, -0.5f }, { 0.0f, 0.0f, -1.0f }, { 0.0f, 1.0f } },
        { { -0.5f,  0.5f, -0.5f }, { 0.0f, 0.0f, -1.0f }, { 0.0f, 0.0f } },
        { {  0.5f,  0.5f, -0.5f }, { 0.0f, 0.0f, -1.0f }, { 1.0f, 0.0f } },
        { {  0.5f, -0.5f, -0.5f }, { 0.0f, 0.0f, -1.0f }, { 1.0f, 1.0f } },

        // Back face
        { { -0.5f, -0.5f,  0.5f }, { 0.0f, 0.0f, 1.0f }, { 1.0f, 1.0f } },
        { {  0.5f, -0.5f,  0.5f }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 1.0f } },
        { {  0.5f,  0.5f,  0.5f }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 0.0f } },
        { { -0.5f,  0.5f,  0.5f }, { 0.0f, 0.0f, 1.0f }, { 1.0f, 0.0f } },

        // Top face
        { { -0.5f,  0.5f, -0.5f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 1.0f } },
        { { -0.5f,  0.5f,  0.5f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f } },
        { {  0.5f,  0.5f,  0.5f }, { 0.0f, 1.0f, 0.0f }, { 1.0f, 0.0f } },
        { {  0.5f,  0.5f, -0.5f }, { 0.0f, 1.0f, 0.0f }, { 1.0f, 1.0f } },

        // Bottom face
        { { -0.5f, -0.5f, -0.5f }, { 0.0f, -1.0f, 0.0f }, { 1.0f, 1.0f } },
        { {  0.5f, -0.5f, -0.5f }, { 0.0f, -1.0f, 0.0f }, { 0.0f, 1.0f } },
        { {  0.5f, -0.5f,  0.5f }, { 0.0f, -1.0f, 0.0f }, { 0.0f, 0.0f } },
        { { -0.5f, -0.5f,  0.5f }, { 0.0f, -1.0f, 0.0f }, { 1.0f, 0.0f } },

        // Left face
        { { -0.5f, -0.5f,  0.5f }, { -1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f } },
        { { -0.5f,  0.5f,  0.5f }, { -1.0f, 0.0f, 0.0f }, { 0.0f, 0.0f } },
        { { -0.5f,  0.5f, -0.5f }, { -1.0f, 0.0f, 0.0f }, { 1.0f, 0.0f } },
        { { -0.5f, -0.5f, -0.5f }, { -1.0f, 0.0f, 0.0f }, { 1.0f, 1.0f } },

        // Right face
        { {  0.5f, -0.5f, -0.5f }, { 1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f } },
        { {  0.5f,  0.5f, -0.5f }, { 1.0f, 0.0f, 0.0f }, { 0.0f, 0.0f } },
        { {  0.5f,  0.5f,  0.5f }, { 1.0f, 0.0f, 0.0f }, { 1.0f, 0.0f } },
        { {  0.5f, -0.5f,  0.5f }, { 1.0f, 0.0f, 0.0f }, { 1.0f, 1.0f } },
    };

    mIndices = {
        // Front face
        0, 1, 2, 0, 2, 3,

        // Back face
        4, 5, 6, 4, 6, 7,

        // Top face
        8, 9, 10, 8, 10, 11,

        // Bottom face
        12, 13, 14, 12, 14, 15,

        // Left face
        16, 17, 18, 16, 18, 19,

        // Right face
        20, 21, 22, 20, 22, 23,
    };
}

これはボックスメッシュを定義したクラスです。

頂点リストとインデックスリストを取得することができます。

このメッシュをペンライトとして使用します。

頂点の構造体は、position、normal、uvを持っています。

今回はGPUインスタンシングのためのクラスを追加します。

GeometryInstancingActorクラス

class GeometryInstancingActor
{
public:
	GeometryInstancingActor(const CubeGeometry& cube, unsigned int instancingCount);
	virtual ~GeometryInstancingActor() override;

	void Initialize(Dx12Wrapper& dx);
	void Draw(Dx12Wrapper& dx, bool isShadow) const;
	void Update();
	void EndOfFrame(Dx12Wrapper& dx);
	int GetIndexCount() const;

private:
	struct GeometryInstanceData
	{
		DirectX::XMFLOAT3 position;
		DirectX::XMFLOAT4 rotation;
		DirectX::XMFLOAT3 scale;
		DirectX::XMFLOAT4 color;

		GeometryInstanceData(const DirectX::XMFLOAT3& position, const DirectX::XMFLOAT4& rotation, const DirectX::XMFLOAT3& scale, const DirectX::XMFLOAT4& color)
		{
			this->position = position;
			this->rotation = rotation;
			this->scale = scale;
			this->color = color;
		}
	};

	void InitializeInstanceData(unsigned int instanceCount);

	HRESULT CreateVertexBufferAndIndexBuffer(Dx12Wrapper& dx);
	HRESULT CreateInstanceBuffer(Dx12Wrapper& dx);
	HRESULT UploadInstanceData(Dx12Wrapper& dx);
	void ChangeInstanceCount(Dx12Wrapper& dx);

private:
	template<typename T>
	using ComPtr = Microsoft::WRL::ComPtr<T>;

	const std::vector<Vertex>& mVertices;
	const std::vector<unsigned int>& mIndices;

	ComPtr<ID3D12Resource> mVertexBuffer = nullptr;
	ComPtr<ID3D12Resource> mIndexBuffer = nullptr;
	D3D12_VERTEX_BUFFER_VIEW mVertexBufferView = {};
	D3D12_INDEX_BUFFER_VIEW mIndexBufferView = {};
	Vertex* mMappedVertex = nullptr;
	unsigned int* mMappedIndex = nullptr;

	ComPtr<ID3D12Resource> mInstanceBuffer = nullptr;
	ComPtr<ID3D12Resource> mInstanceUploadBuffer = nullptr;

	unsigned int mInstanceCount;
	float mInstanceUnit;
	std::vector<GeometryInstanceData> mInstanceData{};

	std::unique_ptr<Transform> mTransform{};

	bool mRefreshInstanceBufferFlag = false;
};

これはGPUインスタンシングを使用するオブジェクトのクラスです。

これまで作成してきたものと大きな違いはありません。

頂点バッファビュー、インデックスバッファビューを持ち、Drawメソッドでレンダリングを行います。

異なる点は、個々のインスタンスの属性用のバッファを持っていることです。

GeometryInstancingActor::GeometryInstancingActor(const CubeGeometry& cube, unsigned int instancingCount = 1):
mVertices(cube.GetVertices()),
mIndices(cube.GetIndices()),
mInstanceCount(instancingCount),
mInstanceUnit(10.0f),
mTransform(new Transform())
{
}

コンストラクタでCubeGeometryを受け取り、頂点とインデックス情報を初期化します。

そして、インスタンシングの数、間隔などを初期化します。

void GeometryInstancingActor::Initialize(Dx12Wrapper& dx)
{
	HRESULT result;
	InitializeInstanceData(mInstanceCount);

	result = CreateVertexBufferAndIndexBuffer(dx);
	assert(SUCCEEDED(result));

	result = CreateInstanceBuffer(dx);
	assert(SUCCEEDED(result));

	result = UploadInstanceData(dx);
	assert(SUCCEEDED(result));
}

初期化メソッドです。

インスタンス情報の初期化

頂点バッファビューとインデックスバッファビューの作成

インスタンス情報バッファの作成

インスタンス情報のアップロード

を行います。

void GeometryInstancingActor::InitializeInstanceData(unsigned int instanceCount)
{
	mInstanceData.clear();
	mInstanceData.reserve(instanceCount);

	const unsigned int rowCount = 50;

	DirectX::XMVECTOR rootPosition = DirectX::XMLoadFloat3(&mTransform->GetPosition());

	int r = 0;
	int c = 0;

  std::random_device randomDevice;
  std::mt19937 generator(randomDevice());
  std::uniform_int_distribution<int> distribution(0, 2);

	DirectX::XMFLOAT4 colors[] =
	{
		DirectX::XMFLOAT4(0.259f, 0.918f, 0.996f, 1.0f),
		DirectX::XMFLOAT4(0.565f, 0.792f, 0.624f, 1.0f),
		DirectX::XMFLOAT4(1.0f, 0.471f, 0.561f, 1.0f)
	};

	for (int i = 0; i < instanceCount; i++)
	{
		DirectX::XMVECTOR positionVector = DirectX::XMVectorAdd(rootPosition, DirectX::XMVectorSet(mInstanceUnit * r, 0.0f, mInstanceUnit * c, 0.0f));
		DirectX::XMFLOAT4 color = colors[distribution(generator)];
		DirectX::XMFLOAT3 position{};
		DirectX::XMStoreFloat3(&position, positionVector);

		mInstanceData.emplace_back(position, mTransform->GetQuaternion(), mTransform->GetScale(), color);
		
		if (i != 0 && i % rowCount == 0)
		{
			c++;
			r = 0;
		}
		else
		{
			r++;
		}
	}
}

インスタンスの個別情報を初期化するコードです。

少し複雑なコードですが、

mTransformの位置を基準にペンライトが一定の間隔で離れて配置されるように位置値を計算します。

そして、3つの色のうち1つをランダムに設定します。

HRESULT GeometryInstancingActor::CreateVertexBufferAndIndexBuffer(Dx12Wrapper& dx)
{
	D3D12_HEAP_PROPERTIES heapProperties = {};
	heapProperties.Type = D3D12_HEAP_TYPE_UPLOAD;
	heapProperties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
	heapProperties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;

	size_t vertexSize = sizeof(Vertex);
	size_t vertexCount = mVertices.size();
	D3D12_RESOURCE_DESC resourceDesc = {};
	resourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
	resourceDesc.Width = vertexCount * vertexSize;
	resourceDesc.Height = 1;
	resourceDesc.DepthOrArraySize = 1;
	resourceDesc.MipLevels = 1;
	resourceDesc.Format = DXGI_FORMAT_UNKNOWN;
	resourceDesc.SampleDesc.Count = 1;
	resourceDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
	resourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;

	auto result = dx.Device()->CreateCommittedResource(
		&heapProperties,
		D3D12_HEAP_FLAG_NONE,
		&resourceDesc,
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(mVertexBuffer.ReleaseAndGetAddressOf()));

	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return result;
	}

	result = mVertexBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedVertex));
	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return result;
	}

	std::copy(std::begin(mVertices), std::end(mVertices), mMappedVertex);
	mVertexBuffer->Unmap(0, nullptr);

	mVertexBufferView.BufferLocation = mVertexBuffer->GetGPUVirtualAddress();
	mVertexBufferView.SizeInBytes = vertexSize * vertexCount;
	mVertexBufferView.StrideInBytes = vertexSize;

	resourceDesc.Width = sizeof(unsigned int) * mIndices.size();

	result = dx.Device()->CreateCommittedResource(
		&heapProperties,
		D3D12_HEAP_FLAG_NONE,
		&resourceDesc,
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(mIndexBuffer.ReleaseAndGetAddressOf()));

	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return result;
	}

	result = mIndexBuffer->Map(0, nullptr, (void**)&mMappedIndex);
	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return result;
	}

	std::copy(std::begin(mIndices), std::end(mIndices), mMappedIndex);
	mIndexBuffer->Unmap(0, nullptr);

	mIndexBufferView.BufferLocation = mIndexBuffer->GetGPUVirtualAddress();
	mIndexBufferView.Format = DXGI_FORMAT_R32_UINT;
	mIndexBufferView.SizeInBytes = sizeof(unsigned int) * mIndices.size();

	return S_OK;
}

頂点バッファビューとインデックスバッファビューを生成するメソッドです。

ここまでの記事をご覧になった方なら、もうこれは馴染み深いものになっているでしょう。

CubeGeometryから得た頂点とインデックスデータを使用して生成します。

HRESULT GeometryInstancingActor::CreateInstanceBuffer(Dx12Wrapper& dx)
{
	auto bufferSize = sizeof(GeometryInstanceData);

	auto heapProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT);
	auto bufferDesc = CD3DX12_RESOURCE_DESC::Buffer(bufferSize * mInstanceCount);

	auto result = dx.Device()->CreateCommittedResource(
		&heapProperties,
		D3D12_HEAP_FLAG_NONE,
		&bufferDesc,
		D3D12_RESOURCE_STATE_COPY_DEST,
		nullptr,
		IID_PPV_ARGS(&mInstanceBuffer)
	);

	if (FAILED(result))
	{
		return result;
	}

	heapProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);

	result = dx.Device()->CreateCommittedResource(
		&heapProperties,
		D3D12_HEAP_FLAG_NONE,
		&bufferDesc,
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(&mInstanceUploadBuffer)
	);

	return result;
}

インスタンスの属性をアップロードするバッファを作成します。

mInstanceBufferを作成する部分が少し特殊です。

ヒープタイプをD3D12_HEAP_TYPE_DEFAULTに設定し、リソース状態をD3D12_RESOURCE_STATE_COPY_DESTに設定しています。

今まで見てきたものとは少し違いますね。

mInstanceUploadBufferは前と同じです。

これまで見てきた方法では、mInstanceUploadBufferのようにアップロードヒープを使用し、Mapでデータを転送していました。

これにより、CPUからGPUリソースに直接アクセスしてデータを書き込むことができました。

そのため、毎フレーム変更される可能性のあるワールドトランスフォームのデータやマテリアル情報などを転送していました。

しかし、アップロードヒープはGPUでのパフォーマンスを考えると相対的に良くありません。

インスタンシングに使用するデータについて考えてみましょう。

1000個のインスタンシングを行うとすれば、1000個の属性が必要になります。

1000個で終わるでしょうか?望めば10000個も可能です。

この観点から見ると、パフォーマンスの低いアップロードヒープは適していません。

そのため、mInstanceBufferのようにデフォルトヒープを使用します。デフォルトヒープはアップロードヒープよりもGPUが高速にアクセスできます。

そして、インスタンスの属性は毎フレーム変更される情報でもないため、変更時に転送すれば十分です。

しかし、デフォルトヒープはCPUから直接データを書き込むことができないため、まずmInstanceUploadBufferにデータをGPUに移し、そのデータを再びmInstanceBufferにコピーする方法を使用します。

そのため、リソース状態をD3D12_RESOURCE_STATE_COPY_DESTに設定しているのです。

HRESULT GeometryInstancingActor::UploadInstanceData(Dx12Wrapper& dx)
{
	GeometryInstanceData* mappedData;

	const size_t instanceDataSize = sizeof(GeometryInstanceData);

	auto result = mInstanceUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mappedData));
	if (FAILED(result))
	{
		return result;
	}

	memcpy(mappedData, mInstanceData.data(), instanceDataSize * mInstanceCount);

	mInstanceUploadBuffer->Unmap(0, nullptr);

	D3D12_SUBRESOURCE_DATA subResourceData = {};
	subResourceData.pData = mappedData;
	subResourceData.RowPitch = instanceDataSize;
	subResourceData.SlicePitch = subResourceData.RowPitch;

	UpdateSubresources(dx.CommandList().Get(), mInstanceBuffer.Get(), mInstanceUploadBuffer.Get(), 0, 0, 1, &subResourceData);

	return S_OK;
}

このメソッドが先ほど説明した内容を処理するメソッドです。

mInstanceUploadBufferをMapを使用してCPUから書き込み可能にし、データをコピーした後、

UpdateSubresourcesを使用してmInstanceUploadBufferの内容をデフォルトヒープであるmInstanceBufferにコピーしています。

UpdateSubresourcesは、アップロードヒープの内容をデフォルトヒープにコピーする際に使用します。

D3D12_SUBRESOURCE_DATAでデータの開始アドレスとデータのバイトサイズを渡します。

これはコマンドリストに記録されるため、即座に作業が完了するわけではなく、非同期的に実行されます。

void GeometryInstancingActor::ChangeInstanceCount(Dx12Wrapper& dx)
{
	InitializeInstanceData(mInstanceCount);

	mInstanceBuffer = nullptr;
	mInstanceUploadBuffer = nullptr;

	auto result = CreateInstanceBuffer(dx);
	assert(SUCCEEDED(result));

	result = UploadInstanceData(dx);
	assert(SUCCEEDED(result));
}

インスタンス数を変更するメソッドも用意しました。

インスタンス数を変更すると、使用していたバッファをすべて解放し、また初期化を行います。

Drawメソッドは、Rendererクラスを作成した後に作成します。

InstancingRenderer

これからレンダラークラスを作成します。

ルートシグネチャとパイプラインステートを作成し、

インスタンシングに適したシェーダーも作成する必要があります。

class InstancingRenderer
{
public:
	InstancingRenderer(Dx12Wrapper& dx);
	~InstancingRenderer();

	void Update();
	void BeforeDrawAtForwardPipeline();
	void Draw();
	void EndOfFrame();
	void AddActor(std::shared_ptr<GeometryInstancingActor> actor);

private:
	HRESULT CreateRootSignature();
	HRESULT CreateGraphicsPipeline();
	bool CheckShaderCompileResult(HRESULT result, ID3DBlob* error = nullptr);

private:
	Dx12Wrapper& mDirectX;

	template<typename T>
	using ComPtr = Microsoft::WRL::ComPtr<T>;

	ComPtr<ID3D12RootSignature> mRootSignature = nullptr;
	ComPtr<ID3D12PipelineState> mForwardPipeline = nullptr;

	std::vector<std::shared_ptr<GeometryInstancingActor>> mActorList = {};
};

宣言部です。

InstancingRenderer::InstancingRenderer(Dx12Wrapper& dx):
mDirectX(dx)
{
	assert(SUCCEEDED(CreateRootSignature()));
	assert(SUCCEEDED(CreateGraphicsPipeline()));
}

コンストラクタでルートシグネチャとパイプラインステートを作成します。

HRESULT InstancingRenderer::CreateRootSignature()
{
	//Scene Buffer
	D3D12_DESCRIPTOR_RANGE sceneBufferDescriptorRange = {};
	sceneBufferDescriptorRange.NumDescriptors = 1;
	sceneBufferDescriptorRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
	sceneBufferDescriptorRange.BaseShaderRegister = 0;
	sceneBufferDescriptorRange.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

	CD3DX12_ROOT_PARAMETER rootParam[2] = {};

	rootParam[0].InitAsDescriptorTable(1, &sceneBufferDescriptorRange, D3D12_SHADER_VISIBILITY_VERTEX);
	rootParam[1].InitAsShaderResourceView(0, 0, D3D12_SHADER_VISIBILITY_VERTEX); // InstanceBuffer;

	CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc = {};
	rootSignatureDesc.Init(_countof(rootParam), rootParam, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

	ComPtr<ID3DBlob> rootSignatureBlob = nullptr;
	ComPtr<ID3DBlob> errorBlob = nullptr;

	auto result = D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1_0, &rootSignatureBlob, &errorBlob);
	if (FAILED(result) == true)
	{
		assert(SUCCEEDED(result));
		return result;
	}

	result = mDirectX.Device()->CreateRootSignature(0, rootSignatureBlob->GetBufferPointer(), rootSignatureBlob->GetBufferSize(), IID_PPV_ARGS(mRootSignature.ReleaseAndGetAddressOf()));
	if (FAILED(result) == true)
	{
		assert(SUCCEEDED(result));
		return result;
	}

	return S_OK;
}

ルートシグネチャを生成するメソッドです。

0番目のルートパラメータはシーンの情報(ビュー、プロジェクション、光など)のコンスタントバッファを設定します。

先ほどインスタンスの属性に関するバッファを生成しましたが、デスクリプタヒープは作成しませんでした。

なぜならデスクリプタヒープを使用せずにバッファだけで設定することができます。

rootParam[1].InitAsShaderResourceView(0, 0, D3D12_SHADER_VISIBILITY_VERTEX); // InstanceBuffer;

このようにすることで、バッファ1つだけでシェーダーリソースビューを設定することができます。

使用するレジスターインデックスを最初のパラメーターで指定します。

インスタンスの属性に関するバッファもシェーダーで使用されるリソースであるため、tレジスターを使用します。

HRESULT InstancingRenderer::CreateGraphicsPipeline()
{
	UINT flags = 0;
#if defined( DEBUG ) || defined( _DEBUG )
	flags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif

	D3D12_INPUT_ELEMENT_DESC inputLayout[] =
	{
		{
			"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0,
			D3D12_APPEND_ALIGNED_ELEMENT,
			D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0
		},
		{
			"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0,
			D3D12_APPEND_ALIGNED_ELEMENT,
			D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0
		},
		{
			"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0,
			D3D12_APPEND_ALIGNED_ELEMENT,
			D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0
		}
	};

	D3D12_GRAPHICS_PIPELINE_STATE_DESC graphicsPipelineDesc = {};

	graphicsPipelineDesc.pRootSignature = mRootSignature.Get();
	graphicsPipelineDesc.SampleMask = D3D12_DEFAULT_SAMPLE_MASK;
	graphicsPipelineDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
	graphicsPipelineDesc.RasterizerState.CullMode = D3D12_CULL_MODE_BACK;
	graphicsPipelineDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
	graphicsPipelineDesc.InputLayout.pInputElementDescs = inputLayout;
	graphicsPipelineDesc.InputLayout.NumElements = _countof(inputLayout);
	graphicsPipelineDesc.IBStripCutValue = D3D12_INDEX_BUFFER_STRIP_CUT_VALUE_DISABLED;
	graphicsPipelineDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
	graphicsPipelineDesc.SampleDesc.Count = 1;
	graphicsPipelineDesc.SampleDesc.Quality = 0;
	graphicsPipelineDesc.DepthStencilState.DepthEnable = true;
	graphicsPipelineDesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
	graphicsPipelineDesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
	graphicsPipelineDesc.DepthStencilState.StencilEnable = false;
	graphicsPipelineDesc.DSVFormat = DXGI_FORMAT_D32_FLOAT;

	graphicsPipelineDesc.RTVFormats[0] = DXGI_FORMAT_B8G8R8A8_UNORM;
	graphicsPipelineDesc.RTVFormats[1] = DXGI_FORMAT_B8G8R8A8_UNORM;
	graphicsPipelineDesc.RTVFormats[2] = DXGI_FORMAT_B8G8R8A8_UNORM;
	graphicsPipelineDesc.NumRenderTargets = 3;

	ComPtr<ID3DBlob> vs = nullptr;
	ComPtr<ID3DBlob> ps = nullptr;
	ComPtr<ID3DBlob> errorBlob = nullptr;

	auto result = D3DCompileFromFile(L"GeometryInstancingVertexShader.hlsl",
		nullptr,
		D3D_COMPILE_STANDARD_FILE_INCLUDE,
		"main",
		"vs_5_0",
		flags,
		0,
		&vs,
		&errorBlob);

	if (!CheckShaderCompileResult(result, errorBlob.Get()))
	{
		assert(0);
		return result;
	}

	result = D3DCompileFromFile(L"GeometryInstancingPixelShader.hlsl",
		nullptr,
		D3D_COMPILE_STANDARD_FILE_INCLUDE,
		"main",
		"ps_5_0",
		flags,
		0,
		&ps,
		&errorBlob);

	if (!CheckShaderCompileResult(result, errorBlob.Get()))
	{
		assert(0);
		return result;
	}

	graphicsPipelineDesc.VS = CD3DX12_SHADER_BYTECODE(vs.Get());
	graphicsPipelineDesc.PS = CD3DX12_SHADER_BYTECODE(ps.Get());

	result = mDirectX.Device()->CreateGraphicsPipelineState(&graphicsPipelineDesc, IID_PPV_ARGS(mForwardPipeline.ReleaseAndGetAddressOf()));
	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return result;
	}

	return S_OK;
}

インプットレイアウトを設定し、パイプラインステートを生成します。

ブレンドは必要なく、デプステストは使用するようにします。

PMXと同様に、3つのレンダーターゲットに出力するようにします。

シェーダーを作成しましょう。

struct InstanceData
{
	float3 position;
	float4 rotation;
	float3 scale;
	float4 color;
};

struct VertexOutput
{
	float4 svpos : SV_POSITION;
	float4 normal : NORMAL0;
	float2 uv : TEXCOORD;
	float4 color : COLOR;
};

struct PixelOutput
{
	float4 color : SV_TARGET0;
	float4 normal : SV_TARGET1;
	float4 highLum : SV_TARGET2;
};

StructuredBuffer<InstanceData> instanceBuffer : register(t0);

cbuffer SceneBuffer : register(b0)
{
	matrix view;
	matrix proj;
	matrix invproj;
	matrix lightCamera;
	matrix shadow;
	float4 lightVec;
	float3 eye;
};

ヘッダーをこのように作成します。

InstanceDataは、先ほどGeometryInstancingActorで定義したインスタンス属性と同じです。

これをStructuredBufferとして宣言します。

StructuredBufferはGPUで複数のデータを管理するためのバッファです。

配列のようにインデックスでアクセスが可能です。

#include "GeometryInstancingHeader.hlsli"
#include "Math.hlsli"

VertexOutput main(
	float4 pos : POSITION,
	float4 normal : NORMAL,
	float2 uv : TEXCOORD,
	uint instNo : SV_InstanceID)
{
	VertexOutput output;

	float4x4 t = Translate(instanceBuffer[instNo].position);
	float4x4 r = QuaternionToMatrix(instanceBuffer[instNo].rotation);
	float4x4 s = ScaleMatrix(instanceBuffer[instNo].scale);

	float4x4 world = mul(t, mul(r, s));

	float4 worldPosition = mul(world, pos);
	output.color = instanceBuffer[instNo].color;
	output.svpos = mul(mul(proj, view), worldPosition);
	output.normal = mul(world, normal);
	output.uv = uv;

	return output;
}

頂点シェーダーです。

入力パラメータにSV_InstanceIDセマンティクスが見えます。

このパラメータがGPUインスタンシングでのインスタンス番号です。

私たちが計算して渡すのではなく、インスタンス描画を行うとGPUが自動的に割り当ててくれます。

この番号を使用してinstanceBufferを参照し、インスタンスごとに異なる値を使用することができますね。

内容自体に特別なものはありません。

位置、回転、スケール値を使って行列を作成し、ワールドトランスフォームを生成します。

そして、ビュー行列、投影行列まで掛け合わせてsvposを計算します。

位置、回転、スケール値を使用して行列を生成するメソッドは、別のMath.hlsliというヘッダーに記述しました。

float4x4 Translate(float3 position)
{
    return float4x4(
        1, 0, 0, position.x,
        0, 1, 0, position.y,
        0, 0, 1, position.z,
        0, 0, 0, 1
        );
}

float4x4 QuaternionToMatrix(float4 q)
{
    float x = q.x, y = q.y, z = q.z, w = q.w;
    float x2 = x + x, y2 = y + y, z2 = z + z;
    float xx = x * x2, xy = x * y2, xz = x * z2;
    float yy = y * y2, yz = y * z2, zz = z * z2;
    float wx = w * x2, wy = w * y2, wz = w * z2;

    return float4x4(
        1.0 - (yy + zz), xy + wz, xz - wy, 0.0,
        xy - wz, 1.0 - (xx + zz), yz + wx, 0.0,
        xz + wy, yz - wx, 1.0 - (xx + yy), 0.0,
        0.0, 0.0, 0.0, 1.0
        );
}

float4x4 ScaleMatrix(float3 scale)
{
    return float4x4(
        scale.x, 0, 0, 0,
        0, scale.y, 0, 0,
        0, 0, scale.z, 0,
        0, 0, 0, 1
        );
}

Math.hlsliの内容です。

#include "GeometryInstancingHeader.hlsli"

PixelOutput main(VertexOutput input) : SV_TARGET
{
	PixelOutput output;

	output.color = input.color;
	output.highLum = input.color;
	output.normal.rgb = float3((input.normal.xyz + 1.0f) / 2.0f);
	output.normal.a = 1.0f;

	return
}

ピクセルシェーダーです。

ペンライトをブルームで光らせたいと思いました。

そのため、受け取った色をそのまま高輝度テクスチャに出力します。

頂点シェーダーでインスタンス番号に応じて色が渡されたので、ペンライトごとに色が異なるでしょう。

パイプラインステートの準備も整いましたので、GeometryInstancingActorのDrawメソッドを作成して描画してみましょう。

auto cmdList = mDirectX.CommandList();
cmdList->SetPipelineState(mForwardPipeline.Get());
cmdList->SetGraphicsRootSignature(mRootSignature.Get());

まず、Drawを呼び出す前に、InstancingRendererでパイプラインステートとルートシグネチャを設定します。

void GeometryInstancingActor::Draw(Dx12Wrapper& dx, bool isShadow) const
{
	dx.SetSceneBuffer(0);
	dx.SetRSSetViewportsAndScissorRectsByScreenSize();

	auto cmdList = dx.CommandList();
	cmdList->SetGraphicsRootShaderResourceView(1, mInstanceBuffer->GetGPUVirtualAddress());
	
	cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	cmdList->IASetVertexBuffers(0, 1, &mVertexBufferView);
	cmdList->IASetIndexBuffer(&mIndexBufferView);
	cmdList->DrawIndexedInstanced(GetIndexCount(), mInstanceCount, 0, 0, 0);
}

Drawメソッドです。

void Dx12Wrapper::SetSceneBuffer(int rootParameterIndex) const
{
	ID3D12DescriptorHeap* sceneHeaps[] = { mSceneDescHeap.Get() };
	mCmdList->SetDescriptorHeaps(1, sceneHeaps);
	mCmdList->SetGraphicsRootDescriptorTable(rootParameterIndex, mSceneDescHeap->GetGPUDescriptorHandleForHeapStart());
}

シーン情報バッファをバインドできるように、外部からルートパラメータを渡せるようにDx12Wrapperにこのようなメソッドを追加しました。

void Dx12Wrapper::SetRSSetViewportsAndScissorRectsByScreenSize() const
{
	const auto windowSize = Application::Instance().GetWindowSize();

	D3D12_VIEWPORT vp = CD3DX12_VIEWPORT(0.0f, 0.0f, windowSize.cx, windowSize.cy);
	mCmdList->RSSetViewports(1, &vp);

	CD3DX12_RECT rc(0, 0, windowSize.cx, windowSize.cy);
	mCmdList->RSSetScissorRects(1, &rc);
}

ビューポートとシザーレクトも設定できるようにしました。

auto cmdList = dx.CommandList();
cmdList->SetGraphicsRootShaderResourceView(1, mInstanceBuffer->GetGPUVirtualAddress());

ここを見ると、先ほど言ったようにディスクリプタヒープを設定せずにバッファだけでバインディングが可能です。

SetGraphicsRootShaderResourceViewを使用してバッファの仮想アドレスだけを渡せばよいのです。

cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
cmdList->IASetVertexBuffers(0, 1, &mVertexBufferView);
cmdList->IASetIndexBuffer(&mIndexBufferView);
cmdList->DrawIndexedInstanced(GetIndexCount(), mInstanceCount, 0, 0, 0);

DrawIndexedInstancedのパラメータを見てください。

これまでは2番目のパラメータに1を渡していましたが、インスタンス数を渡しています。

このようにすると、その数だけインスタンシングを行います。

void InstancingRenderer::Draw()
{
	for (auto& actor : mActorList)
	{
		actor->Draw(mDirectX, false);
	}
}

InstancingRendererでDrawを呼び出すようにします。

描画

それでは、ビルドして結果を見てみましょう。

image.png

私は2000個をインスタンシングしました。

image 1.png

これはブルーム効果を有効にしたものです。

image 2.png
PIXでも1回の描画呼命令で2000個が描画されているのを確認できます。

ペンライトアニメーション

ペンライトが静止しているのはつまらないですね。

実際のコンサート会場でペンライトを振っているように見せてみましょう。

CPUでアニメーション処理をせず、シェーダーで行います。

GPUインスタンシングを通じて並列的に計算されるます。

https://www.youtube.com/watch?v=iC3QKBW9EmA

この内容は、Unity Japanで紹介された内容に基づいています。

ペンライトの動きについては、この動画の内容と全く同じ方法を使用するので、一度ご覧ください。

動画の内容に従って三角関数を使用してペンライトを回転させたりします。

そして、このようにアニメーションさせるためには、時間をシェーダーに渡す必要があります。

シェーダーに時間を渡すためのコンスタントバッファを作成します。

struct GlobalParameterBuffer
{
	float time;
};

Dx12Wrapperにパラメーターバッファとして使用する構造体を追加します。

現在は時間しかありませんが、後で何か様々なグローバルなパラメーターをシェーダーに渡したい場合は、ここに追加すればすぐに使用できるでしょう。

HRESULT Dx12Wrapper::CreateGlobalParameterBuffer()
{
	auto heapProp = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);

	unsigned int size = sizeof(GlobalParameterBuffer);
	unsigned int alignment = D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT;
	unsigned int width = size + alignment - (size % alignment);

	auto resourceDesc = CD3DX12_RESOURCE_DESC::Buffer(width);

	auto result = mDevice->CreateCommittedResource(
		&heapProp,
		D3D12_HEAP_FLAG_NONE,
		&resourceDesc,
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(mGlobalParameterBuffer.ReleaseAndGetAddressOf()));

	return result;
}

Dx12Wrapper にパラメーター送信用のバッファを作成します。

時間のようなものは、今回だけでなく、他の場所でもシェーダーで使用することになるでしょう。

void Dx12Wrapper::SetGlobalParameterBuffer(unsigned rootParameterIndex) const
{
	mCmdList->SetGraphicsRootConstantBufferView(rootParameterIndex, mGlobalParameterBuffer->GetGPUVirtualAddress());
}

このように外部からルートパラメータを渡して設定できるようにメソッドを追加します。

このパラメータコンスタントバッファもディスクリプタヒープなしで使用します。

シェーダーリソースとは異なり、SetGraphicsRootConstantBufferViewを使用します。

void Dx12Wrapper::UpdateGlobalParameterBuffer() const
{
	GlobalParameterBuffer* mapped;
	
	const unsigned int current = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now().time_since_epoch()).count();
	
	mGlobalParameterBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mapped));
	mapped->time = current * 0.001f;
	mGlobalParameterBuffer->Unmap(0, nullptr);
}

std::chronoを使用して現在時間を取得します。

ミリ秒単位で取得したので、秒単位に変換してデータを送信します。

ルートパラメータもInstancingRendererで修正しましょう。

HRESULT InstancingRenderer::CreateRootSignature()
{
	//Scene Buffer
	D3D12_DESCRIPTOR_RANGE sceneBufferDescriptorRange = {};
	sceneBufferDescriptorRange.NumDescriptors = 1;
	sceneBufferDescriptorRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
	sceneBufferDescriptorRange.BaseShaderRegister = 0;
	sceneBufferDescriptorRange.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

	CD3DX12_ROOT_PARAMETER rootParam[3] = {};

	rootParam[0].InitAsDescriptorTable(1, &sceneBufferDescriptorRange, D3D12_SHADER_VISIBILITY_VERTEX);
	rootParam[1].InitAsShaderResourceView(0, 0, D3D12_SHADER_VISIBILITY_VERTEX); // InstanceBuffer;
	rootParam[2].InitAsConstantBufferView(1, 0, D3D12_SHADER_VISIBILITY_VERTEX); // GlobalParameterBuffer

	...
}

b1レジスタを使用するコンスタントバッファビューを2番目のインデックスに設定します。

cbuffer GlobalParameterBuffer : register(b1)
{
	float time;
}

シェーダーヘッダーにバッファを追加します。

void GeometryInstancingActor::Draw(Dx12Wrapper& dx, bool isShadow) const
{
	dx.SetSceneBuffer(0);
	dx.SetGlobalParameterBuffer(2);
	dx.SetRSSetViewportsAndScissorRectsByScreenSize();

	auto cmdList = dx.CommandList();
	cmdList->SetGraphicsRootShaderResourceView(1, mInstanceBuffer->GetGPUVirtualAddress());
	
	cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	cmdList->IASetVertexBuffers(0, 1, &mVertexBufferView);
	cmdList->IASetIndexBuffer(&mIndexBufferView);
	cmdList->DrawIndexedInstanced(GetIndexCount(), mInstanceCount, 0, 0, 0);
}

GeometryInstancingActorのDrawメソッドでSetGlobalParameterBufferを呼び出すようにします。

頂点シェーダーをペンライトが動くように修正しましょう。

VertexOutput main(
	float4 pos : POSITION,
	float4 normal : NORMAL,
	float2 uv : TEXCOORD,
	uint instNo : SV_InstanceID)
{
	VertexOutput output;

	float4x4 t = Translate(instanceBuffer[instNo].position);
	float4x4 r = QuaternionToMatrix(instanceBuffer[instNo].rotation);
	float4x4 s = ScaleMatrix(instanceBuffer[instNo].scale);

	float frequency = 0.6f;
	float angle = sin(2 * PI * frequency * time + noise);

	float3 zAxis = float3(0, 0, 1);

	float4x4 swingRotation = AngleAxis(angle, zAxis);

	r = mul(r, swingRotation);

	float4x4 world = mul(t, mul(r, s));

	float4x4 swingTranslate = Translate(float3(0.0, 2.0f, 0.0f));

	world = mul(world, swingTranslate);

	float4 worldPosition = mul(world, pos);
	output.color = instanceBuffer[instNo].color;
	output.svpos = mul(mul(proj, view), worldPosition);
	output.normal = mul(world, normal);
	output.uv = uv;

	return output;
}

動画のようにペンライトが動くように適用します。
Swing.gif

ペンライトが動くようになりました。

しかし、すべて同じ動きをしているので、少し単調です。

動画の内容のように乱数とノイズを使用できるようにコードを追加しましょう。

そして乱数のためのシード値も必要です。

このシード値はインスタンスごとに違う必要があります。そうすることで動きが違く見えるでしょう。

そのため、このようなメソッドを使用してシード値の変化を増幅させます。

float RandomInstanceSeed(uint instanceID)
{
    return sin(float(instanceID) * 12.9898) * 43758.5453;
}
float seed = RandomInstanceSeed(instNo);

使用する際は、このようにインスタンス番号でシードを生成できますね。

float Random(float2 seed, float min, float max)
{
    float randomno = frac(sin(dot(seed, float2(12.9898, 78.233))) * 43758.5453);
    return lerp(min, max, randomno);
}

float NoiseRandom(float2 uv)
{
	return frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);
}

float NoiseInterpolate(float a, float b, float t)
{
    return (1.0 - t) * a + (t * b);
}

float ValueNoise(float2 uv)
{
    float2 i = floor(uv);
    float2 f = frac(uv);
    f = f * f * (3.0 - 2.0 * f);

    uv = abs(frac(uv) - 0.5);
    float2 c0 = i + float2(0.0, 0.0);
    float2 c1 = i + float2(1.0, 0.0);
    float2 c2 = i + float2(0.0, 1.0);
    float2 c3 = i + float2(1.0, 1.0);
    float r0 = NoiseRandom(c0);
    float r1 = NoiseRandom(c1);
    float r2 = NoiseRandom(c2);
    float r3 = NoiseRandom(c3);

    float bottomOfGrid = NoiseInterpolate(r0, r1, f.x);
    float topOfGrid = NoiseInterpolate(r2, r3, f.x);
    float t = NoiseInterpolate(bottomOfGrid, topOfGrid, f.y);

    return t;
}

float SimpleNoise(float2 uv, float scale)
{
    float t = 0.0;

    float freq = pow(2.0, float(0));
    float amp = pow(0.5, float(3 - 0));
    t += ValueNoise(float2(uv.x * scale / freq, uv.y * scale / freq)) * amp;

    freq = pow(2.0, float(1));
    amp = pow(0.5, float(3 - 1));
    t += ValueNoise(float2(uv.x * scale / freq, uv.y * scale / freq)) * amp;

    freq = pow(2.0, float(2));
    amp = pow(0.5, float(3 - 2));
    t += ValueNoise(float2(uv.x * scale / freq, uv.y * scale / freq)) * amp;

    return t;
}

そして、乱数生成とノイズ乱数を生成するコードも追加します。

VertexOutput main(
	float4 pos : POSITION,
	float4 normal : NORMAL,
	float2 uv : TEXCOORD,
	uint instNo : SV_InstanceID)
{
	VertexOutput output;

	float4x4 t = Translate(instanceBuffer[instNo].position);
	float4x4 r = QuaternionToMatrix(instanceBuffer[instNo].rotation);
	float4x4 s = ScaleMatrix(instanceBuffer[instNo].scale);

	float seed = RandomInstanceSeed(instNo);
	float random = Random(float2(seed, seed), -100000, 100000);
	float noise = SimpleNoise(float2(random, time * 0.3f), 3.0f);

	float frequency = 0.6f;
	float angle = sin(2 * PI * frequency * time + noise);

	float3 zAxis = float3(r._31, r._32, r._33);
	float random2 = Random(float2(seed, seed), -100000, 100000);
	float noise2 = SimpleNoise(float2(random2, time * 0.3f), 3.0f);
	float random3 = Random(float2(seed, seed), -1, 1);
	zAxis = normalize(float3(random3 * noise2, 0, 1));

	angle *= Random(float2(seed, seed), 0.5f, 1.0f);

	float4x4 swingRotation = AngleAxis(angle, zAxis);

	r = mul(r, swingRotation);

	float4x4 world = mul(t, mul(r, s));

	float4x4 swingTranslate = Translate(float3(0.0, 2.0f, 0.0f));

	world = mul(world, swingTranslate);

	float4 worldPosition = mul(world, pos);
	output.color = instanceBuffer[instNo].color;
	output.svpos = mul(mul(proj, view), worldPosition);
	output.normal = mul(world, normal);
	output.uv = uv;

	return output;
}

頂点シェーダーは最終的にこのように修正されるでしょう。

ノイズ乱数を使用してペンライトのアニメーション速度や軸を変更しています。

swinng.gif
コンサート会場のような雰囲気になりましたね。

まとめ

cons.gif

今回はGPUインスタンシングを使用してペンライトを描画し、アニメーションも適用してコンサート会場のような雰囲気を作り出しました。

少し残念な点は、ペンライトの色がブルーム効果を適用すると白く焼き付いてしまうことですが、これはHDRとトーンマッピングを使用してより美しく改善できるでしょう。

余裕のある方は、そのような方法で直接改善してみるのも良いでしょう。

今回はここまでとします。

次の記事ではリフレクションについて話します。ありがとうございました。

参考リンク

https://youtu.be/iC3QKBW9EmA
https://docs.unity3d.com/Packages/com.unity.shadergraph@6.9/manual/Random-Range-Node.html

https://docs.unity3d.com/Packages/com.unity.shadergraph@7.1/manual/Simple-Noise-Node.html

次回

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?