🖥️ DirectX 개인 강좌&방법론/DirectX12 방법론

DirectX12에서 텍스트를 렌더링하는 2가지 방법 소개

Mawile 2022. 8. 8.
728x90

안녕하세요.

오랜만에 강좌글이 돌아왔습니다 ..!

강좌라기보다는 방법론에 관한 글이라고 하는게 더 정확하겠군요.

이번 포스팅에서는 DirectX12에서 직접 사용자정의 폰트를 통한 텍스트를 출력하는 방법에 관하여 알아보겠습니다.

예를들어서 이런식으로요.

 

그러면.... 바로 시작하겠습니다..!!

 

1번째방법. 메쉬위에 텍셀좌표를 조절해서 그리기

이 방법은 우선, 직접 해야할것들이 많습니다.

대신 2번째방법보다 오버헤드가 훨씬 적으며, 좀더 세부적으로 글씨나 문자에 대하여 다룰수 있습니다.

 

이 방법은 우선 적 정점버퍼를 가진 메쉬를 생성하는데에서 시작합니다.

1-1. 메쉬만들기
void BuildFont()
{
	mFontData["original"] = TextFont::LoadFontData("fontdata.txt");

	std::vector<Shader::TextVertex> vertices(gMaxNumTextCharacters * 4);
	std::vector<std::uint32_t> indices(gMaxNumTextCharacters * 6);

	for (size_t i = 0, k = 0; i < indices.size(); i += 6, k += 4)
	{
		// 0 1
		// 2 3

		indices[i + 0] = (u32)k + 0;
		indices[i + 1] = (u32)k + 1;
		indices[i + 2] = (u32)k + 2;

		indices[i + 3] = (u32)k + 1;
		indices[i + 4] = (u32)k + 3;
		indices[i + 5] = (u32)k + 2;
	}

	UINT vertexBufferSize = sizeof(Shader::TextVertex) * (UINT)vertices.size();
	UINT indexBufferSize = sizeof(std::uint32_t) * (UINT)indices.size();

	auto meshGeometry = std::make_unique<Shader::MeshGeometry>();
	meshGeometry->name = "textGeo";

	meshGeometry->VertexCpu = nullptr;
	meshGeometry->VertexGpu = nullptr;

	HR(D3DCreateBlob(indexBufferSize, meshGeometry->IndexCpu.GetAddressOf()));
	CopyMemory(meshGeometry->IndexCpu->GetBufferPointer(), indices.data(), indexBufferSize);

	meshGeometry->IndexGpu = Utils::CreateDefaultResource(mDevice.Get(), mGraphicsCommandList.Get(),
		indices.data(), indexBufferSize, meshGeometry->IndexGpuUploader);

	meshGeometry->VertexBufferByteSize = vertexBufferSize;
	meshGeometry->VertexByteStride = sizeof(Shader::TextVertex);

	meshGeometry->IndexFormat = DXGI_FORMAT_R32_UINT;
	meshGeometry->IndexBufferByteSize = indexBufferSize;

	Shader::SubmeshGeometry subMeshGeo = {};
	subMeshGeo.IndexCount = (UINT)indices.size();
	subMeshGeo.BaseVertexLocation = 0;
	subMeshGeo.StartIndexLocation = 0;

	meshGeometry->DrawArgs["text"] = subMeshGeo;

	mDrawArgs[meshGeometry->name] = std::move(meshGeometry);
}

여기서 LoadFontData함수의 동작과정이 궁금하실텐데, 그냥 폰트정보텍스트파일에 맞춰서 폰트정보읽는함수입니다.

이부분은 Rastertek강좌를 참고했습니다.

std::vector<Shader::FontType> LoadFontData(const char* filename)
{
    std::ifstream fin;
    int i;
    char temp;


    std::vector<Shader::FontType> font(95);

    fin.open(filename);
    if (fin.fail())
    {
        return (std::vector<Shader::FontType>)0;
    }

    for (i = 0; i < 95; i++)
    {
        fin.get(temp);
        while (temp != ' ')
        {
            fin.get(temp);
        }
        fin.get(temp);
        while (temp != ' ')
        {
            fin.get(temp);
        }

        fin >> font[i].left;
        fin >> font[i].right;
        fin >> font[i].size;
    }

    fin.close();

    return font;
}

또한, 폰트데이터파일 내용입니다.

32   0.0        0.0         0
33 ! 0.0        0.000976563 1
34 " 0.00195313 0.00488281  3
35 # 0.00585938 0.0136719   8
36 $ 0.0146484  0.0195313   5
37 % 0.0205078  0.0302734   10
38 & 0.03125    0.0390625   8
39 ' 0.0400391  0.0410156   1
40 ( 0.0419922  0.0449219   3
41 ) 0.0458984  0.0488281   3
42 * 0.0498047  0.0546875   5
43 + 0.0556641  0.0625      7
44 , 0.0634766  0.0644531   1
45 - 0.0654297  0.0683594   3
46 . 0.0693359  0.0703125   1
47 / 0.0712891  0.0751953   4
48 0 0.0761719  0.0820313   6
49 1 0.0830078  0.0859375   3
50 2 0.0869141  0.0927734   6
51 3 0.09375    0.0996094   6
52 4 0.100586   0.106445    6
53 5 0.107422   0.113281    6
54 6 0.114258   0.120117    6
55 7 0.121094   0.126953    6
56 8 0.12793    0.133789    6
57 9 0.134766   0.140625    6
58 : 0.141602   0.142578    1
59 ; 0.143555   0.144531    1
60 < 0.145508   0.151367    6
61 = 0.152344   0.15918     7
62 > 0.160156   0.166016    6
63 ? 0.166992   0.171875    5
64 @ 0.172852   0.18457     12
65 A 0.185547   0.194336    9
66 B 0.195313   0.202148    7
67 C 0.203125   0.209961    7
68 D 0.210938   0.217773    7
69 E 0.21875    0.225586    7
70 F 0.226563   0.232422    6
71 G 0.233398   0.241211    8
72 H 0.242188   0.249023    7
73 I 0.25       0.250977    1
74 J 0.251953   0.256836    5
75 K 0.257813   0.265625    8
76 L 0.266602   0.272461    6
77 M 0.273438   0.282227    9
78 N 0.283203   0.290039    7
79 O 0.291016   0.298828    8
80 P 0.299805   0.306641    7
81 Q 0.307617   0.31543     8
82 R 0.316406   0.323242    7
83 S 0.324219   0.331055    7
84 T 0.332031   0.338867    7
85 U 0.339844   0.34668     7
86 V 0.347656   0.356445    9
87 W 0.357422   0.370117    13
88 X 0.371094   0.37793     7
89 Y 0.378906   0.385742    7
90 Z 0.386719   0.393555    7
91 [ 0.394531   0.396484    2
92 \ 0.397461   0.401367    4
93 ] 0.402344   0.404297    2
94 ^ 0.405273   0.410156    5
95 _ 0.411133   0.417969    7
96 ` 0.418945   0.420898    2
97 a 0.421875   0.426758    5
98 b 0.427734   0.432617    5
99 c 0.433594   0.438477    5
100 d 0.439453  0.444336    5
101 e 0.445313  0.450195    5
102 f 0.451172  0.455078    4
103 g 0.456055  0.460938    5
104 h 0.461914  0.466797    5
105 i 0.467773  0.46875     1
106 j 0.469727  0.472656    3
107 k 0.473633  0.478516    5
108 l 0.479492  0.480469    1
109 m 0.481445  0.490234    9
110 n 0.491211  0.496094    5
111 o 0.49707   0.501953    5
112 p 0.50293   0.507813    5
113 q 0.508789  0.513672    5
114 r 0.514648  0.517578    3
115 s 0.518555  0.523438    5
116 t 0.524414  0.527344    3
117 u 0.52832   0.533203    5
118 v 0.53418   0.539063    5
119 w 0.540039  0.548828    9
120 x 0.549805  0.554688    5
121 y 0.555664  0.560547    5
122 z 0.561523  0.566406    5
123 { 0.567383  0.570313    3
124 | 0.571289  0.572266    1
125 } 0.573242  0.576172    3
126 ~ 0.577148  0.583984    7

 

그다음 인덱스버퍼를 생성하는 원리를 설명드리겠습니다.

저가 위에 코드에서 표기해놓은 인덱스버퍼의 순서와 패턴입니다.

우선 DirectX12는 Rasterizer에서 FrontClockWise옵션을 건드리지 않는한 무조건 인덱스버퍼는 시계방향으로 가져갑니다.

또한, 우리는 이 방법에서는 텍스트 하나당 삼각형 2개를 사용하여 사각형을 만든뒤, 그 위에 텍스쳐를 입혀서 글자를 렌더링할것입니다.

따라서 우리가 넣어야할 인덱스버퍼의 순서는 0->1->2, 1->3->2 가 됩니다.

그리고 인덱스버퍼는 채워줘야 하지만, 정점버퍼는 비워두시는걸 추천드립니다.

어차피 이후에 계속 정점버퍼에 관한 정보를 바꿔야하는데  곧 바뀔값에 굳이 데이터를 채울필요는 없기때문입니다.

 

위 코드에서 Utils::CreateDefaultResource함수는 다음과 같은 기능을 제공합니다.

Microsoft::WRL::ComPtr<ID3D12Resource> CreateDefaultResource(
    ID3D12Device* device,
    ID3D12GraphicsCommandList* cmdList,
    const void* data,
    size_t size,
    Microsoft::WRL::ComPtr<ID3D12Resource>& uploadHeap)
{
    Microsoft::WRL::ComPtr<ID3D12Resource> defaultHeap = nullptr;

    HR(device->CreateCommittedResource(&unmove(CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT)), D3D12_HEAP_FLAG_NONE,
        &unmove(CD3DX12_RESOURCE_DESC::Buffer(size)), D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(defaultHeap.GetAddressOf())));
    HR(device->CreateCommittedResource(&unmove(CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD)), D3D12_HEAP_FLAG_NONE,
        &unmove(CD3DX12_RESOURCE_DESC::Buffer(size)), D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(uploadHeap.GetAddressOf())));

    D3D12_SUBRESOURCE_DATA subResourceData = {};
    subResourceData.pData = data;
    subResourceData.RowPitch = (LONG_PTR)size;
    subResourceData.SlicePitch = subResourceData.RowPitch;

    cmdList->ResourceBarrier(1, &unmove(CD3DX12_RESOURCE_BARRIER::Transition(defaultHeap.Get(),
        D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_DEST)));
    UpdateSubresources<1>(cmdList, defaultHeap.Get(), uploadHeap.Get(), 0, 0, 1, &subResourceData);
    cmdList->ResourceBarrier(1, &unmove(CD3DX12_RESOURCE_BARRIER::Transition(defaultHeap.Get(),
        D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_GENERIC_READ)));

    return defaultHeap;
}

그냥 업로드힙을 만들어서 gpu로 넘겨줄 리소스를 제출합니다.

 

1-2. 사용할 폰트 이미지를 텍스쳐로 바꾼뒤, 서술자힙을 만들어 srv에 제출한다.

사실 아래의 코드 이전에, dds파일이나 png파일을 2D텍스쳐로 바꿔주는작업을 해줬습니다.

저는 따로 png파일을 DirectX12 2D텍스쳐로 바꿔주는 함수를 만들었는데, 여러분에게 도움이 될만한 레퍼런스를 남겨놓을테니, 여러분도 png파일을 2DTexture로 바꿔보시기 바랍니다.

void BuildDescriptor()
{
	D3D12_DESCRIPTOR_HEAP_DESC srvHeapDescriptor = {};
	srvHeapDescriptor.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
	srvHeapDescriptor.NodeMask = 0;
	srvHeapDescriptor.NumDescriptors = 2;
	srvHeapDescriptor.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;

	HR(mDevice->CreateDescriptorHeap(&srvHeapDescriptor, IID_PPV_ARGS(mSrvHeap.GetAddressOf())));
	CD3DX12_CPU_DESCRIPTOR_HANDLE srvHandle(mSrvHeap->GetCPUDescriptorHandleForHeapStart());

	D3D12_SHADER_RESOURCE_VIEW_DESC srViewDescriptor = {};
	srViewDescriptor.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
	srViewDescriptor.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
	srViewDescriptor.Texture2D.MostDetailedMip = 0;
	srViewDescriptor.Texture2D.ResourceMinLODClamp = 0.0f;

	auto textTexture = mTextures["text"].get()->ResourceGpuHeap;
	srViewDescriptor.Format = textTexture->GetDesc().Format;
	srViewDescriptor.Texture2D.MipLevels = textTexture->GetDesc().MipLevels;

	mDevice->CreateShaderResourceView(textTexture.Get(), &srViewDescriptor, srvHandle);
	srvHandle.Offset(1, mCbvSrvUavSize);
}

이 코드는 텍스쳐자원을 미리 얻어온후, 서술자힙에 srv를 만듭니다.

 

1-3. 셰이더 만들기

사실 당연한 이야기이지만, 일반적인 정물을 렌더링하는 셰이더랑 텍스쳐를 렌더링하는 셰이더랑 나눠놨습니다.

하지만, 이 방법론에서는 텍스쳐를 렌더링하는 셰이더만 소개합니다.

Texture2D gTextures[1] : register(t0);

SamplerState gSamPointWrap : register(s0);
SamplerState gSamPointClamp : register(s1);
SamplerState gSamLinearWrap : register(s2);
SamplerState gSamLinearClamp : register(s3);
SamplerState gSamAnisotropicWrap : register(s4);
SamplerState gSamAnisotropicClamp : register(s5);

cbuffer cbPerObject : register(b0)
{
	float4x4 gWorld;
	float4x4 gTexTransform;

	uint gMatIndex;
	float3 cbPerObjPadding1;
};

cbuffer cbPerPass : register(b1)
{
	float4x4 gView;
	float4x4 gInvView;
	float4x4 gProj;
	float4x4 gInvProj;
	float4x4 gViewProj;
	float4x4 gInvViewProj;
	float4x4 gViewOrtho;

	float4 gAmbientLight;
	float3 gEyePos;
	float gTotalTime;

	float gDeltaTime;
	float3 cbPerPassPadding1;

	float4 gColor;
};

struct VertexIn
{
	float3 PosL		: POSITION;
	float2 TexCoord : TEXCOORD;
};

struct VertexOut
{
	float4 PosNDC	: SV_POSITION;
	float3 PosW		: POSITION;
	float2 TexCoord : TEXCOORD;
};

VertexOut VS(VertexIn vin)
{
	VertexOut vout = (VertexOut)0.0f;

	// Position
	float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
	vout.PosW = posW.xyz;
	vout.PosNDC = mul(posW, gViewOrtho);

	// TexCoord
	vout.TexCoord = vin.TexCoord;

	return vout;
}

float4 PS(VertexOut vin) : SV_TARGET
{
	float4 color = gTextures[gMatIndex].Sample(gSamAnisotropicWrap, vin.TexCoord);

	if (color.r == 0.0f)
	{
		clip(color.a);
	}
	else
	{
		color.rgb = color.rgb;
		color.a = 1.0f;
	}

	return color;
}

사실 정점셰이더는 그냥 지역좌표계를 ndc좌표계로 변환해주는 역할만 해주고, 우리가 눈여겨 볼곳은 픽셀셰이더입니다.

우선 픽셀셰이더에서는 폰트가 적힌 이미지에서 특정구간의 텍셀을 따옵니다.

만약 그 텍셀의 red부분이 0이면(= 이미지에서 글자부분이 아닌 빈 공간이라면) alpha부분을 짤라서 표시하지않습니다.

저가 사용한 폰트이미지는 다음과 같습니다. 보시면은 빈 공간은 검은색인데 이때 검은색이므로 r값이 0이겠죠?

그래서 다음과 같이 구현했답니다.

By Rastertek Tutorials.

 

1-4. PSO 서술자 만들기

우선 우리가 사용할 기본적인 PSO서술자의 형태입니다.

D3D12_GRAPHICS_PIPELINE_STATE_DESC graphicsPSODescriptor = {};
graphicsPSODescriptor.NodeMask = 0;
graphicsPSODescriptor.InputLayout = { mInputLayouts["layout1"].data(), (UINT)mInputLayouts["layout1"].size() };
graphicsPSODescriptor.pRootSignature = mRootSignatrue.Get();
graphicsPSODescriptor.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
graphicsPSODescriptor.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
graphicsPSODescriptor.DSVFormat = gDepthStencilFormat;
graphicsPSODescriptor.NumRenderTargets = 1;
graphicsPSODescriptor.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
graphicsPSODescriptor.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
graphicsPSODescriptor.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
graphicsPSODescriptor.RTVFormats[0] = gBackBufferFormat;
graphicsPSODescriptor.SampleMask = UINT_MAX;
graphicsPSODescriptor.SampleDesc = { 1, 0 };
graphicsPSODescriptor.VS = {
	reinterpret_cast<BYTE*>(mShaders["vs"]->GetBufferPointer()),
	mShaders["vs"]->GetBufferSize()
};
graphicsPSODescriptor.PS = {
	reinterpret_cast<BYTE*>(mShaders["ps"]->GetBufferPointer()),
	mShaders["ps"]->GetBufferSize()
};

하지만, 그냥 이렇게 사용하기에는 몇가지 수정할 사항이있습니다.

우선 우리는 실제 텍스쳐 뒤의 배경을 삭제해야 하기때문에 Blending과정을 거쳐야하며, 저는 정점셰이더에서 NDC좌표계로 변환할때 투영행렬을 직교투영행렬을 사용할것이므로, 깊이버퍼를 꺼주었습니다.

D3D12_GRAPHICS_PIPELINE_STATE_DESC textPSODescriptor = graphicsPSODescriptor;
textPSODescriptor.BlendState.AlphaToCoverageEnable = FALSE;
textPSODescriptor.BlendState.IndependentBlendEnable = FALSE;
textPSODescriptor.BlendState.RenderTarget[0].BlendEnable = TRUE;

textPSODescriptor.BlendState.RenderTarget[0].SrcBlend = D3D12_BLEND_ONE;
textPSODescriptor.BlendState.RenderTarget[0].SrcBlendAlpha = D3D12_BLEND_ONE;
textPSODescriptor.BlendState.RenderTarget[0].DestBlend = D3D12_BLEND_SRC_ALPHA;
textPSODescriptor.BlendState.RenderTarget[0].DestBlendAlpha = D3D12_BLEND_ZERO;
textPSODescriptor.BlendState.RenderTarget[0].BlendOp = D3D12_BLEND_OP_ADD;
textPSODescriptor.BlendState.RenderTarget[0].BlendOpAlpha = D3D12_BLEND_OP_ADD;
textPSODescriptor.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;

textPSODescriptor.DepthStencilState.DepthEnable = FALSE;

 

1-5. 시야행렬은 고정시키고, 투영행렬은 원근투영대신 직교투영을 사용하자.

당연한 이야기이지만, 화면상에서 텍스트가 움직이지 않으려면, 텍스트전용으로 고정된 시야 행렬이 필요합니다.

void UpdatePassCB()
{
	DirectX::XMMATRIX View = DirectX::XMLoadFloat4x4(&mCamera.View());
	DirectX::XMMATRIX Proj = DirectX::XMLoadFloat4x4(&mCamera.Projection());
	DirectX::XMMATRIX ViewProj = DirectX::XMMatrixMultiply(View, Proj);

	DirectX::XMMATRIX ViewOrtho = DirectX::XMMatrixMultiply(
		DirectX::XMMatrixLookAtLH(DirectX::XMVectorSet(0.0f, 0.0f, -10.0f, 1.0f),
			DirectX::XMVectorSet(0.0f, 0.0f, 1.0f, 1.0f)
			, DirectX::XMVectorSet(0.0f, 1.0f, 0.0f, 1.0f)),
		DirectX::XMMatrixOrthographicLH((float)mClientWidth, (float)mClientHeight, 1.0f, 1000.0f));

	PassConstants passConstants = {};
	DirectX::XMStoreFloat4x4(&passConstants.View, DirectX::XMMatrixTranspose(View));
	DirectX::XMStoreFloat4x4(&passConstants.InvView, DirectX::XMMatrixTranspose(DirectX::XMMatrixInverse(&DirectX::XMMatrixDeterminant(View), View)));
	DirectX::XMStoreFloat4x4(&passConstants.Proj, DirectX::XMMatrixTranspose(Proj));
	DirectX::XMStoreFloat4x4(&passConstants.InvProj, DirectX::XMMatrixTranspose(DirectX::XMMatrixInverse(&DirectX::XMMatrixDeterminant(Proj), Proj)));
	DirectX::XMStoreFloat4x4(&passConstants.ViewProj, DirectX::XMMatrixTranspose(ViewProj));
	DirectX::XMStoreFloat4x4(&passConstants.InvViewProj, DirectX::XMMatrixTranspose(DirectX::XMMatrixInverse(&DirectX::XMMatrixDeterminant(ViewProj), ViewProj)));
	DirectX::XMStoreFloat4x4(&passConstants.ViewOrtho, DirectX::XMMatrixTranspose(ViewOrtho));
	passConstants.EyePos = { 0.0f, 0.0f, 0.0f };
	passConstants.DeltaTime = mGameTimer.DeltaTime();
	passConstants.TotalTime = mGameTimer.TotalTime();

	passConstants.AmbientLight = { 0.25f, 0.25f, 0.35f, 1.0f };
	passConstants.Lights[0].Direction = { 0.57735f, -0.57735f, 0.57735f };
	passConstants.Lights[0].Strength = { 0.6f, 0.6f, 0.6f };
	passConstants.Lights[1].Direction = { -0.57735f, -0.57735f, 0.57735f };
	passConstants.Lights[1].Strength = { 0.3f, 0.3f, 0.3f };
	passConstants.Lights[2].Direction = { 0.0f, -0.707f, -0.707f };
	passConstants.Lights[2].Strength = { 0.15f, 0.15f, 0.15f };

	passConstants.Color = { 0.0f, 1.0f, 1.0f, 1.0f };

	mCurrFrameResource->mPassCB->CopyData(0, passConstants);
}

 

이 함수는 따로 프레임자원에다가 업로드 힙을 만들어서 gpu에 데이터를 제출하는 함수입니다.

이때, 텍스트셰이더에서 사용할 뷰-투영행렬이 ViewOrtho인데, 잘보시면 위치와 방향이 고정된 뷰행렬을 사용하며,

원근투영이아닌 직교투영을 사용합니다.

 

1-F. 정점버퍼에 텍스트의 특정텍셀을 렌더링해주기

우선 이 메소드에서는 컴퓨터의 xy좌표(코드에서는 positionX, positionY)를 기반으로 적절한 텍셀좌표와 정점의 지역좌표를 변환하고 업데이트합니다.

void UpdateTextVB()
{
	using Shader::TextVertex;

	std::string sentence = "GameTime: " + std::to_string(mGameTimer.TotalTime()) + " seconds";
	int numLetters = (int)sentence.size();

	if (numLetters >= gMaxNumTextCharacters)
	{
		throw std::runtime_error("sentence >= gMaxNumTextCharacters");
	}

	float positionX = 10.0f, positionY = 10.0f;
	std::vector<TextVertex> vertices(numLetters * 4);
	float drawX = (float)(((float)mClientWidth / 2.0f) * -1.0f) + positionX;
	float drawY = (float)((float)mClientHeight / 2.0f) - positionY;
	BuildVertexArray(mFontData["original"], vertices.data(), sentence.c_str(), drawX, drawY, 3.0f, 32.0f);

	for (size_t i = 0; i < vertices.size(); ++i)
	{
		mCurrFrameResource->mTextVB->CopyData((UINT)i, vertices[i]);
	}


	mTextVB->meshGeo->VertexGpu = mCurrFrameResource->mTextVB->Resource();
	mTextVB->NumframeDirty = Shader::gNumFrameResources;
}

mClientWidth와 mClientHeight는 각각 스크린의 가로, 세로길이입니다.

이제 BuildVertexArray라는 함수의 내용이 궁금하실겁니다.

이 함수는 rastertek에서 힌트를 얻어왔지만, 저의 디자인방식대로 개편했습니다.

void BuildVertexArray(const std::vector<Shader::FontType>& font, void* data, const char* sentence,
    float drawX, float drawY, float scaleX, float scaleY)
{
    Shader::TextVertex* vertices = (Shader::TextVertex*)data;
    size_t numLetters = strlen(sentence);

    for (size_t i = 0, k= 0; i < numLetters; ++i, k += 4)
    {
        int letter = ((int)sentence[i] - 32);

        if (letter == 0)
        {
            drawX += 3.0f + scaleX;
        }
        else
        {
            // 0 1
            // 2 3

            // top left
            vertices[k + 0].Position = DirectX::XMFLOAT3(drawX, drawY, 0.0f);
            vertices[k + 0].TexCoord = DirectX::XMFLOAT2(font[letter].left, 0.0f);

            // top right
            vertices[k + 1].Position = DirectX::XMFLOAT3(drawX + font[letter].size + scaleX, drawY, 0.0f);
            vertices[k + 1].TexCoord = DirectX::XMFLOAT2(font[letter].right, 0.0f);

            // bottom left
            vertices[k + 2].Position = DirectX::XMFLOAT3(drawX, drawY - scaleY, 0.0f);
            vertices[k + 2].TexCoord = DirectX::XMFLOAT2(font[letter].left, 0.8f);

            // bottom right
            vertices[k + 3].Position = DirectX::XMFLOAT3(drawX + font[letter].size + scaleX, drawY - scaleY, 0.0f);
            vertices[k + 3].TexCoord = DirectX::XMFLOAT2(font[letter].right, 0.8f);

            drawX += font[letter].size + 1.0f + scaleX;
        }
    }
}

뭔가 익숙한게 보이실겁니다.

아까 1-1에서 소개했던 내용 그대로, 인덱스버퍼를 기반으로 정점버퍼를 업데이트합니다.

만약 여러분이 위의 과정을 모두 거치셨다면, 우리는 다음과 같은 글자를 렌더링할 수 있습니다.

만약 여러분이 여기서 글자에 텍스쳐를 입히고싶으시다면, 픽셀셰이더를 다음과 같이 수정해주세요.

당연히, 여러분이 원하시는 텍스쳐를 서술자힙에 업로드해야되겠죠?

float4 PS(VertexOut vin) : SV_TARGET
{
	float4 color = gTextures[gMatIndex].Sample(gSamAnisotropicWrap, vin.TexCoord);
	float4 wood = gTextures[1].Sample(gSamAnisotropicWrap, vin.TexCoord); // 내가 원하는 텍스쳐

	if (color.r == 0.0f)
	{
		clip(color.a);
	}
	else
	{
		color.rgb = wood.rgb;
		color.a = 1.0f;
	}

	return color;
}

 

자세한 구현과정및 전체 프로젝트를 보고싶으신분은 저의 github로 방문해주세요.

https://github.com/orangelie/Dx12UIFont

 

GitHub - orangelie/Dx12UIFont: DirectX12-based user interface and font.

DirectX12-based user interface and font. Contribute to orangelie/Dx12UIFont development by creating an account on GitHub.

github.com

 

2번째방법. D2D의 렌더타켓 리소스를 D3D12로 불러오기

두번째 방법은, 첫번째방법에 비해 오버헤드가 일어날 확률이 크지만, 정말 구현하기 간단하며, 다양한 폰트, 색깔, 한글, 일본어 등등 여러가지 편한점이 많습니다.

 

2-1. InfoQueue로 Windows11의 DirectX12 호환성 버그 제거

우선 이거는 저가 직접 겪은오류들때문에 넣은 부분입니다.

당연히 ID3D12Device를 만드신후에 적용하시기바랍니다.

#if defined(_DEBUG)
	Microsoft::WRL::ComPtr<ID3D12InfoQueue> infoQueue;
	HR(m12Device->QueryInterface(infoQueue.GetAddressOf()));

	/* Removing INVALID_HANDLE_VALUE Warning Statements from Debugging Layer. */
	D3D12_MESSAGE_SEVERITY messageSeverity[] = {
		D3D12_MESSAGE_SEVERITY_INFO
	};

	D3D12_MESSAGE_ID messageID[] = {
		D3D12_MESSAGE_ID_INVALID_DESCRIPTOR_HANDLE
	};

	D3D12_INFO_QUEUE_FILTER infoQueueFilter = {};
	infoQueueFilter.DenyList.pSeverityList = messageSeverity;
	infoQueueFilter.DenyList.NumSeverities = _countof(messageSeverity);
	infoQueueFilter.DenyList.pIDList = messageID;
	infoQueueFilter.DenyList.NumIDs = _countof(messageID);

	HR(infoQueue->PushStorageFilter(&infoQueueFilter));

	/* DXSampleHelper.h > Remove warning statements that appear when "throw" in the debugging layer. */
	infoQueue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_ERROR, true);
	infoQueue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_CORRUPTION, true);
	infoQueue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_WARNING, false);

	/* D3D12 ERROR: ID3D12CommandQueue::Present: Resource state (0x800: D3D12_RESOURCE_STATE_COPY_SOURCE) (promoted from COMMON state) of resource (0x000001F6BE05D070:'Unnamed ID3D12Resource Object') (subresource: 0) must be in COMMON state when transitioning to use in a different Command List type, because resource state on previous Command List type : D3D12_COMMAND_LIST_TYPE_COPY, is actually incompatible and different from that on the next Command List type : D3D12_COMMAND_LIST_TYPE_DIRECT. [ RESOURCE_MANIPULATION ERROR #990: RESOURCE_BARRIER_MISMATCHING_COMMAND_LIST_TYPE]
D3D12: **BREAK** enabled for the previous message, which was: [ ERROR RESOURCE_MANIPULATION #990: RESOURCE_BARRIER_MISMATCHING_COMMAND_LIST_TYPE ]. */
	// This is a bug in the DXGI Debug Layer interaction with the DX12 Debug Layer Windows 11.
	// SOLUTION> https://stackoverflow.com/questions/69805245/directx-12-application-is-crashing-in-windows-11
	D3D12_MESSAGE_ID hide[] =
	{
		D3D12_MESSAGE_ID_MAP_INVALID_NULLRANGE,
		D3D12_MESSAGE_ID_UNMAP_INVALID_NULLRANGE,
		// Workarounds for debug layer issues on hybrid-graphics systems
		D3D12_MESSAGE_ID_EXECUTECOMMANDLISTS_WRONGSWAPCHAINBUFFERREFERENCE,
		D3D12_MESSAGE_ID_RESOURCE_BARRIER_MISMATCHING_COMMAND_LIST_TYPE,
	};
	D3D12_INFO_QUEUE_FILTER filter = {};
	filter.DenyList.NumIDs = static_cast<UINT>(std::size(hide));
	filter.DenyList.pIDList = hide;
	infoQueue->AddStorageFilterEntries(&filter);

	infoQueue->Release();
#endif

 

2-2. D3D11On12Device + D3D11DeviceContext 만들기

우리는 ID3D11Device를 통해 ID3D11On12Device객체를 얻을수있습니다.

Microsoft::WRL::ComPtr<ID3D11Device> device11 = nullptr;
	HR(D3D11On12CreateDevice(m12Device.Get(),
		d3d11DeviceFlag, nullptr, 0, reinterpret_cast<IUnknown**>(mCommandQueue.GetAddressOf()),
		1, 0, device11.GetAddressOf(), mImmediateContext.GetAddressOf(), nullptr));
	HR(device11.As(&m11On12Device));

 

2-3. D2D1객체만들고, 렌더 타켓 뷰로 dx12자원을 dx11자원으로 래핑하기.

우리는 그다음, IDWriteFactory객체를 얻어야합니다.

이 객체는 다음에 우리가, 폰트에 대한 포맷정보를 바꿀때 사용가능합니다.

그다음 CPU서술자힙에 대한 구조체를 만듭니다.

이 구조체를 통해 우리는 스왑체인으로부터 렌더타켓뷰에 관한 리소스를 얻어올수있습니다.

{
	D2D1_DEVICE_CONTEXT_OPTIONS deviceContextOption = D2D1_DEVICE_CONTEXT_OPTIONS_NONE;
	HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, __uuidof(ID2D1Factory3), &factoryOptions, &mD2D1Factory));
	Microsoft::WRL::ComPtr<IDXGIDevice> device;
	HR(m11On12Device.As(&device));
	HR(mD2D1Factory->CreateDevice(device.Get(), mD2D1Device.GetAddressOf()));
	HR(mD2D1Device->CreateDeviceContext(deviceContextOption, &mD2D1DeviceContext));
	HR(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), &mDWriteFactory));
}

FLOAT dpiX, dpiY;
#pragma warning(push)
#pragma warning(disable:4996)
mD2D1Factory->GetDesktopDpi(&dpiX, &dpiY);
#pragma warning(pop)

D2D1_BITMAP_PROPERTIES1 bitmapProperties = D2D1::BitmapProperties1(D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW,
	D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_PREMULTIPLIED), dpiX, dpiY);

{
	D3D12_DESCRIPTOR_HEAP_DESC rtvDescriptor = {};
	rtvDescriptor.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
	rtvDescriptor.NodeMask = 0;
	rtvDescriptor.NumDescriptors = gBackBufferCount;
	rtvDescriptor.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;

	HR(m12Device->CreateDescriptorHeap(&rtvDescriptor, IID_PPV_ARGS(mRtvDescriptorHeap.GetAddressOf())));
	mRtvDescriptorHeapSize = m12Device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
}

CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(mRtvDescriptorHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < gBackBufferCount; ++i)
{
	HR(mSwapChain->GetBuffer(i, IID_PPV_ARGS(mBackBufferPointer[i].GetAddressOf())));
	m12Device->CreateRenderTargetView(mBackBufferPointer[i].Get(), nullptr, rtvHandle);

	D3D11_RESOURCE_FLAGS resourceFlags = { D3D11_BIND_RENDER_TARGET };
	/*
	HR(m11On12Device->CreateWrappedResource(
		mBackBufferPointer[i].Get(), &resourceFlags,
		D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT,
		IID_PPV_ARGS(mWrappedBackbuffer[i].GetAddressOf())));
	*/
	HR(m11On12Device->CreateWrappedResource(
		mBackBufferPointer[i].Get(), &resourceFlags,
		D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_PRESENT,
		IID_PPV_ARGS(mWrappedBackbuffer[i].GetAddressOf())));

	Microsoft::WRL::ComPtr<IDXGISurface> surface;
	HR(mWrappedBackbuffer[i].As(&surface));
	HR(mD2D1DeviceContext->CreateBitmapFromDxgiSurface(surface.Get(), &bitmapProperties, mD2D1Backbuffer[i].GetAddressOf()));

	rtvHandle.Offset(1, mRtvDescriptorHeapSize);
}

여기서 제일 눈여겨봐야할점은 11On12Device를 통해 미리 래핑된 D3D11리소스를 만든다는 점입니다.

이 리소스에 접근하여 우리가 폰트를 만들고 텍스트를 그려서 렌더링될것입니다.

 

2-4. 폰트 정보를 만든다.

이제 아까 만든 IDWriteFactory객체를 통해, 텍스트에 관한 포맷을 만듭니다.

스타일은 Italic으로 했습니다. 폰트이름은 "Verdana"를 사용했으며, 언어체계는 영어입니다.

폰트의 크기는 25로 설정했으며, 색은 하늘색입니다.

void D3D11On12::LoadPipeline()
{
	{
		HR(mD2D1DeviceContext->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Aqua), mSolidColorBrush.GetAddressOf()));
		HR(mDWriteFactory->CreateTextFormat(L"Verdana", nullptr,
			DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_ITALIC, DWRITE_FONT_STRETCH_NORMAL,
			25, L"en-us", mDWriteTextFormat.GetAddressOf()));

		mDWriteTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
		mDWriteTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
	}
}

 

2-F 텍스트를 렌더링한다.
void D3D11On12::RenderUI()
{
	/*
		D2D DEBUG WARNING - EndDraw was called, but the rendertarget was not in a valid state. This may result from calling EndDraw without a matching BeginDraw.
		D2D DEBUG WARNING - A Flush call by a render target failed [88990001]. Tags [0x0, 0x0].
		D3D11 WARNING: ID3D11DeviceContext::OMSetRenderTargets: Resource being set to OM RenderTarget slot 0 is inaccessible because of a previous call to ReleaseSync or GetDC. [ STATE_SETTING WARNING #9: DEVICE_OMSETRENDERTARGETS_HAZARD]
		D2D DEBUG ERROR - An attempt to draw to an inaccessible target has been detected.
	*/

	D2D1_SIZE_F rtSize = mD2D1Backbuffer[mCurrBackbufferIndex]->GetSize();
	D2D1_RECT_F textRect = D2D1::RectF(0.0f, 0.0f, rtSize.width, rtSize.height);
	static const WCHAR text[] = L"헤헿 D3D11On12 프로젝트 입니다.";

	m11On12Device->AcquireWrappedResources(mWrappedBackbuffer[mCurrBackbufferIndex].GetAddressOf(), 1);
	
	mD2D1DeviceContext->SetTarget(mD2D1Backbuffer[mCurrBackbufferIndex].Get());
	mD2D1DeviceContext->BeginDraw();
	mD2D1DeviceContext->SetTransform(D2D1::Matrix3x2F::Identity());
	mD2D1DeviceContext->DrawTextA(text, _countof(text) - 1, mDWriteTextFormat.Get(), &textRect, mSolidColorBrush.Get());
	mD2D1DeviceContext->EndDraw();

	m11On12Device->ReleaseWrappedResources(mWrappedBackbuffer[mCurrBackbufferIndex].GetAddressOf(), 1);
	mImmediateContext->Flush();
}

마지막부분입니다.

ID3D11On12Device::AcquireWrappedResources 메소드는

아까 D3D12에서 D3D11로 렌더타켓뷰로 래핑된 리소스에 대한 접근권한을 취득합니다.

 

그다음, D2D1 장치문맥을 통해, 실제 텍스트를 그립니다.

이때 텍스트의 월드행렬, 장치문맥이 그릴 리소스타겟을 설정한뒤 텍스트를 그려야만합니다.

 

ID3D11On12Device::ReleaseWrappedResources 메소드 EndDraw를 통해 모든 그리기명령종료, 리소스 접근권한을 해제했다면, 마지막으로 해당 D3D11명령들을 모두 ID3D12CommandQueue로 제출하기위해 ID3D11DeviceContext::Flush()메서드를 호출합니다.

 

그러면 다음과 같은 결과를 얻을 수 있습니다.

 

 

자세한 구현과정및 전체 프로젝트를 보고싶으신분은 저의 github로 방문해주세요.

https://github.com/orangelie/MSDirectXAnalysis

 

GitHub - orangelie/MSDirectXAnalysis: Microsoft Sample DirectX Project Example Analysis.

Microsoft Sample DirectX Project Example Analysis. - GitHub - orangelie/MSDirectXAnalysis: Microsoft Sample DirectX Project Example Analysis.

github.com

 

궁금한점이 있다면 추가로 댓글로 질문주시기 바랍니다.

지금까지 저의 포스팅을 봐주셔서 감사합니다 !

 

 

728x90

댓글