그래픽스/DirectX 12

[DirectX 12 3D게임 입문] Chapter 6.13: Direct3D의 그리기 연산 - 연습문제

KANTAM 2023. 11. 16. 17:17

2. 상자 예제를, 정점 버퍼 두 개를 사용해서 파이프라인에 정점들을 공급하도록 수정하라.

결론적으로 정점과 다른 입력 슬롯으로 색상 버퍼를 파이프라인에 묶어야 한다. 그러기 위해서는 색상을 위한 버퍼와 해당 버퍼의 뷰를 만들어서 IASetVertexBuffers 함수를 호출해야 한다.

 

BoxApp 클래스에 색상 버퍼를 위한 멤버 변수를 만들어 준다.

class BoxApp : public D3DApp
{
...
private:
    Microsoft::WRL::ComPtr<ID3DBlob> ColorBufferCPU = nullptr;
    Microsoft::WRL::ComPtr<ID3D12Resource> ColorBufferGPU = nullptr;
    Microsoft::WRL::ComPtr<ID3D12Resource> ColorBufferUploader = nullptr;
...
};

BuildShadersAndInputLayout 함수에서 mInputLayout 배열을 수정한다. 

void BoxApp::BuildShadersAndInputLayout()
{
...
    mInputLayout =
    {
        { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
        { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
    };
}

BuildBoxGeometry 함수에서 정점과 색상 array를 생성하고, 그에 맞는 버퍼를 만든다. 이때 위에서 추가했던 멤버 변수에 생성한 버퍼를 대입한다.

void BoxApp::BuildBoxGeometry()
{
    std::array<VPosData, 8> vertices =
    {
        VPosData({ XMFLOAT3(-1.0f, -1.0f, -1.0f) }),
        VPosData({ XMFLOAT3(-1.0f, +1.0f, -1.0f) }),
        VPosData({ XMFLOAT3(+1.0f, +1.0f, -1.0f) }),
        VPosData({ XMFLOAT3(+1.0f, -1.0f, -1.0f) }),
        VPosData({ XMFLOAT3(-1.0f, -1.0f, +1.0f) }),
        VPosData({ XMFLOAT3(-1.0f, +1.0f, +1.0f) }),
        VPosData({ XMFLOAT3(+1.0f, +1.0f, +1.0f) }),
        VPosData({ XMFLOAT3(+1.0f, -1.0f, +1.0f) })
    };

    std::array<VColorData, 8> colors =
    {
        VColorData({XMFLOAT4(Colors::White) }),
        VColorData({XMFLOAT4(Colors::Black) }),
        VColorData({XMFLOAT4(Colors::Red) }),
        VColorData({XMFLOAT4(Colors::Green) }),
        VColorData({XMFLOAT4(Colors::Blue) }),
        VColorData({XMFLOAT4(Colors::Yellow) }),
        VColorData({XMFLOAT4(Colors::Cyan) }),
        VColorData({XMFLOAT4(Colors::Magenta) })
    };

    // 정점의 색인들
	std::array<std::uint16_t, 36> indices =
	{
		// front face
		0, 1, 2,
		0, 2, 3,

		// back face
		4, 6, 5,
		4, 7, 6,

		// left face
		4, 5, 1,
		4, 1, 0,

		// right face
		3, 2, 6,
		3, 6, 7,

		// top face
		1, 5, 6,
		1, 6, 2,

		// bottom face
		4, 0, 3,
		4, 3, 7
	};

    const UINT vbByteSize = (UINT)vertices.size() * sizeof(VPosData);
    const UINT cvByteSize = (UINT)colors.size() * sizeof(VColorData);
	const UINT ibByteSize = (UINT)indices.size() * sizeof(std::uint16_t);

	mBoxGeo = std::make_unique<MeshGeometry>();
	mBoxGeo->Name = "boxGeo";

	ThrowIfFailed(D3DCreateBlob(vbByteSize, &mBoxGeo->VertexBufferCPU));
	CopyMemory(mBoxGeo->VertexBufferCPU->GetBufferPointer(), vertices.data(), vbByteSize);

    ThrowIfFailed(D3DCreateBlob(cvByteSize, &ColorBufferCPU));
    CopyMemory(ColorBufferCPU->GetBufferPointer(), colors.data(), cvByteSize);

	ThrowIfFailed(D3DCreateBlob(ibByteSize, &mBoxGeo->IndexBufferCPU));
	CopyMemory(mBoxGeo->IndexBufferCPU->GetBufferPointer(), indices.data(), ibByteSize);

    // 정점 버퍼를 만든다.
	mBoxGeo->VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
		mCommandList.Get(), vertices.data(), vbByteSize, mBoxGeo->VertexBufferUploader);

    // 색상 버퍼를 만든다.
    ColorBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
        mCommandList.Get(), colors.data(), cvByteSize, ColorBufferUploader);

    // 색인 버퍼를 만든다.
	mBoxGeo->IndexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
		mCommandList.Get(), indices.data(), ibByteSize, mBoxGeo->IndexBufferUploader);

	mBoxGeo->VertexByteStride = sizeof(VPosData);
	mBoxGeo->VertexBufferByteSize = vbByteSize;
    ColorByteStride = sizeof(VColorData);
    ColorBufferByteSize = cvByteSize;
	mBoxGeo->IndexFormat = DXGI_FORMAT_R16_UINT;
	mBoxGeo->IndexBufferByteSize = ibByteSize;

    // 색인들을 SubmeshGeometry에 세팅한다.
	SubmeshGeometry submesh;
	submesh.IndexCount = (UINT)indices.size();
	submesh.StartIndexLocation = 0;
	submesh.BaseVertexLocation = 0;

	mBoxGeo->DrawArgs["box"] = submesh;
}

Draw 함수에서 파이프라인에 색상 버퍼를 묶기 위해 IASetVertexBuffers 함수를 추가한다.

void BoxApp::Draw(const GameTimer& gt)
{
...
	mCommandList->IASetVertexBuffers(0, 1, &mBoxGeo->VertexBufferView());
    // 색상 버퍼 묶기
	mCommandList->IASetVertexBuffers(1, 1, &ColorBufferView());
...
}

ColorBufferView 함수는 색상 버퍼를 위한 뷰를 생성하는 함수이다. 

D3D12_VERTEX_BUFFER_VIEW BoxApp::ColorBufferView() const
{
    D3D12_VERTEX_BUFFER_VIEW vbv;
    vbv.BufferLocation = ColorBufferGPU->GetGPUVirtualAddress();
    vbv.StrideInBytes = ColorByteStride;
    vbv.SizeInBytes = ColorBufferByteSize;

    return vbv;
}

이제 실행 시 이전과 동일한 육면체가 생성된다.

 

3. 위상구조를 달리해서 상자를 생성하자.

mCommandList->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

IASetPrimitiveTopology 함수의 인자만 달리하면 된다.

선 띠
삼각형 띠

 

4. 사각뿔의 정점 목록과 색인 목록을 구축해서 그려보라. 밑 면 정점들은 녹색으로, 꼭대기 정점은 빨간색으로 설정하라.

    std::array<VPosData, 5> vertices =
    {
        VPosData({ XMFLOAT3(-1.0f, -1.0f, -1.0f) }),
        VPosData({ XMFLOAT3(+1.0f, -1.0f, -1.0f) }),
        VPosData({ XMFLOAT3(-1.0f, -1.0f, +1.0f) }),
        VPosData({ XMFLOAT3(+1.0f, -1.0f, +1.0f) }),
        VPosData({ XMFLOAT3(+0.0f, +1.0f, +0.0f) })
    };

    std::array<VColorData, 5> colors =
    {
        VColorData({XMFLOAT4(Colors::Green) }),
        VColorData({XMFLOAT4(Colors::Green) }),
        VColorData({XMFLOAT4(Colors::Green) }),
        VColorData({XMFLOAT4(Colors::Green) }),
        VColorData({XMFLOAT4(Colors::Red) })
    };

    std::array<std::uint16_t, 18> indices =
    {
        0, 1, 2,
        1, 3, 2,

        0, 4, 1,

        1, 4, 3, 

        3, 4, 2,
        2, 4, 0
    };

 

6. 상자 예제를, 정점 셰이더에서 정점을 세계 공간으로 변환하기 전에 다음과 같은 변환을 적용하도록 수정하라. 

vin.PosL.xy += 0.5f * sin(vin.PosL.x) * sin(3.0f * gTime);
vin.PosL.z *= 0.6f + 0.4f * sin(2.0f * gTime);

위의 코드를 color.hlsl의 정점 셰이더에 추가한다. 그리고 위의 코드에서 gTime이라는 상수 버퍼 변수가 사용되고 있다. 이를 추가해줘야 한다. 

cbuffer cbPerObject : register(b0)
{
	float4x4 gWorldViewProj; 
	float gTime;			// 추가
};

BoxApp.cpp의 ObjectConstants에도 추가한다.

struct ObjectConstants
{
    XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
    float Time;
};

그리고 Update 함수에서 이 상수 버퍼를 갱신하는데, 이 변수는 현재 GameTimer::TotalTIme() 값에 대응된다. 해당 내용을 추가한다.

void BoxApp::Update(const GameTimer& gt)
{
    // Convert Spherical to Cartesian coordinates.
    float x = mRadius*sinf(mPhi)*cosf(mTheta);
    float z = mRadius*sinf(mPhi)*sinf(mTheta);
    float y = mRadius*cosf(mPhi);

    XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
    XMVECTOR target = XMVectorZero();
    XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);

    XMMATRIX view = XMMatrixLookAtLH(pos, target, up);
    XMStoreFloat4x4(&mView, view);

    XMMATRIX world = XMLoadFloat4x4(&mWorld);
    XMMATRIX proj = XMLoadFloat4x4(&mProj);
    XMMATRIX worldViewProj = world*view*proj;

	ObjectConstants objConstants;
    XMStoreFloat4x4(&objConstants.WorldViewProj, XMMatrixTranspose(worldViewProj));
    objConstants.Time = gt.TotalTime();			// 추가
    mObjectCB->CopyData(0, objConstants);       // 상수 버퍼 mObjectCB를 갱신
}

박스가 꿀렁꿀렁

 

7. 상자 정점들과 피라미드 정점들을 하나의 큰 정점 버퍼와 하나의 색인 버퍼로 병합하여 동시에 그리기

일단 정점과 색인을 하나로 합친다. 

    std::array<VPosData, 13> vertices =
    {
        VPosData({ XMFLOAT3(-1.0f, -1.0f, -1.0f) }),
        VPosData({ XMFLOAT3(-1.0f, +1.0f, -1.0f) }),
        VPosData({ XMFLOAT3(+1.0f, +1.0f, -1.0f) }),
        VPosData({ XMFLOAT3(+1.0f, -1.0f, -1.0f) }),
        VPosData({ XMFLOAT3(-1.0f, -1.0f, +1.0f) }),
        VPosData({ XMFLOAT3(-1.0f, +1.0f, +1.0f) }),
        VPosData({ XMFLOAT3(+1.0f, +1.0f, +1.0f) }),
        VPosData({ XMFLOAT3(+1.0f, -1.0f, +1.0f) }),

        VPosData({ XMFLOAT3(-1.0f, -1.0f, -1.0f) }),
        VPosData({ XMFLOAT3(+1.0f, -1.0f, -1.0f) }),
        VPosData({ XMFLOAT3(-1.0f, -1.0f, +1.0f) }),
        VPosData({ XMFLOAT3(+1.0f, -1.0f, +1.0f) }),
        VPosData({ XMFLOAT3(+0.0f, +1.0f, +0.0f) })
    };

    std::array<VColorData, 13> colors =
    {
        VColorData({XMFLOAT4(Colors::White) }),
        VColorData({XMFLOAT4(Colors::Black) }),
        VColorData({XMFLOAT4(Colors::Red) }),
        VColorData({XMFLOAT4(Colors::Green) }),
        VColorData({XMFLOAT4(Colors::Blue) }),
        VColorData({XMFLOAT4(Colors::Yellow) }),
        VColorData({XMFLOAT4(Colors::Cyan) }),
        VColorData({XMFLOAT4(Colors::Magenta) }),

        VColorData({XMFLOAT4(Colors::Green) }),
        VColorData({XMFLOAT4(Colors::Green) }),
        VColorData({XMFLOAT4(Colors::Green) }),
        VColorData({XMFLOAT4(Colors::Green) }),
        VColorData({XMFLOAT4(Colors::Red) })
    };

    // 정점의 색인들
	std::array<std::uint16_t, 54> indices =
	{
		// front face
		0, 1, 2,
		0, 2, 3,

		// back face
		4, 6, 5,
		4, 7, 6,

		// left face
		4, 5, 1,
		4, 1, 0,

		// right face
		3, 2, 6,
		3, 6, 7,

		// top face
		1, 5, 6,
		1, 6, 2,

		// bottom face
		4, 0, 3,
		4, 3, 7,

        // 피라미드
        0, 1, 2,
        1, 3, 2,

        0, 4, 1,

        1, 4, 3,

        3, 4, 2,
        2, 4, 0
	};

피라미드를 위한 SubMeshGeometry 구조체 변수를 하나 더 만들어서 DrawArgs에 추가한다.

    SubmeshGeometry submesh_pyramid;
    submesh_pyramid.IndexCount = (UINT)(indices.size() - 36);
    submesh_pyramid.StartIndexLocation = 36;
    submesh_pyramid.BaseVertexLocation = 8;
    
    mBoxGeo->DrawArgs["pyramid"] = submesh_pyramid;

이제 상자와 피라미드가 함께 그려지지만, 겹쳐있게 된다. 상자와 피라미드의 세계 변환 행렬을 달리해서 그려야 겹치는걸 방지할 수 있다. 그러기 위해선 상수 버퍼가 2개 필요하므로 상수 버퍼 뷰를 하나 추가하여 만들어야 한다. 

 

상수 버퍼를 위한 서술자 힙에 상수 버퍼가 2개 들어갈 것이므로 NumDescriptors를 2로하여 서술자 힙을 생성한다.

void BoxApp::BuildDescriptorHeaps()
{
    D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
    cbvHeapDesc.NumDescriptors = 2;		// 상수 버퍼 뷰가 2개 담길 것이다. 
    cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
    cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; 
	cbvHeapDesc.NodeMask = 0;
    ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&cbvHeapDesc,
        IID_PPV_ARGS(&mCbvHeap)));
}

상수 버퍼를 2개 생성하여 서술자 힙에 넣는다. 서술자 힙을 CD3DX12_CPU_DESCRIPTOR_HANDLE로 접근하여 Offset을 달리하여 추가한다.

// 상수 버퍼들을 만드는 함수 그리고 서술자 힙에 담긴다.
void BoxApp::BuildConstantBuffers()
{
	mObjectCB = std::make_unique<UploadBuffer<ObjectConstants>>(md3dDevice.Get(), 2, true);

	UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));

    // 버퍼 자체의 시작 주소
	D3D12_GPU_VIRTUAL_ADDRESS cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress();
    // 버퍼에 담길 i번째 상수 버퍼의 오프셋
    int boxCBufIndex = 0;
	cbAddress += boxCBufIndex*objCBByteSize;

	D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
	cbvDesc.BufferLocation = cbAddress;
	cbvDesc.SizeInBytes = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
    
	md3dDevice->CreateConstantBufferView(
		&cbvDesc,
		mCbvHeap->GetCPUDescriptorHandleForHeapStart());

    // 피라미드를 위한 두번째 상수 버퍼 뷰 생성
    cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress();
    boxCBufIndex = 1;
    cbAddress += boxCBufIndex * objCBByteSize;

    cbvDesc.BufferLocation = cbAddress;

    // CD3DX12_CPU_DESCRIPTOR_HANDLE으로 서술자 힙에 접근하여 Offset을 수정한다.
    CD3DX12_CPU_DESCRIPTOR_HANDLE handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(mCbvHeap->GetCPUDescriptorHandleForHeapStart());
    handle.Offset(boxCBufIndex, mCbvSrvUavDescriptorSize);

    md3dDevice->CreateConstantBufferView(
        &cbvDesc,
        handle);
}

이제 상수 버퍼에 담길 mObjectCB에 세계 변환 행렬을 하나 더 추가해서 넣어야 한다. Update 함수에서 mWorld의 (4, 1)을 수정하여 물체가 적절히 이동하도록 설정하자.

void BoxApp::Update(const GameTimer& gt)
{
    // Convert Spherical to Cartesian coordinates.
    float x = mRadius*sinf(mPhi)*cosf(mTheta);
    float z = mRadius*sinf(mPhi)*sinf(mTheta);
    float y = mRadius*cosf(mPhi);

    // 시야 행렬을 구축한다.
    XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
    XMVECTOR target = XMVectorZero();
    XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);

    XMMATRIX view = XMMatrixLookAtLH(pos, target, up);
    XMStoreFloat4x4(&mView, view);
    
    mWorld._41 = -1.2f;

    XMMATRIX world = XMLoadFloat4x4(&mWorld);
    XMMATRIX proj = XMLoadFloat4x4(&mProj);
    XMMATRIX worldViewProj = world*view*proj;

	ObjectConstants objConstants;
    XMStoreFloat4x4(&objConstants.WorldViewProj, XMMatrixTranspose(worldViewProj));
    objConstants.Time = gt.TotalTime();
    mObjectCB->CopyData(0, objConstants);

    mWorld._41 = 1.2f;
    world = XMLoadFloat4x4(&mWorld);
    worldViewProj = world * view * proj;
    XMStoreFloat4x4(&objConstants.WorldViewProj, XMMatrixTranspose(worldViewProj));
    mObjectCB->CopyData(1, objConstants);
}

이제 Draw 함수에서 상자와 피라미드 각각의 상수 버퍼를 적용하고 물체를 그린다. CD3DX12_GPU_DESCRIPTOR_HANDLE로 상수 버퍼 서술자 힙에 접근하여 Offset을 이동시키고 명령 목록에 SetGraphicsRootDescriptorTable로 해당 상수 버퍼를 적용할 수 있다. 

void BoxApp::Draw(const GameTimer& gt)
{
...
    // 상자와 피라미드 각각의 상수 버퍼를 적용하고 그린다.
    CD3DX12_GPU_DESCRIPTOR_HANDLE cbv(mCbvHeap->GetGPUDescriptorHandleForHeapStart());
    cbv.Offset(0, mCbvSrvUavDescriptorSize);
    mCommandList->SetGraphicsRootDescriptorTable(0, cbv);
    mCommandList->DrawIndexedInstanced(
		mBoxGeo->DrawArgs["box"].IndexCount, 
		1, 0, 0, 0);

    cbv.Offset(1, mCbvSrvUavDescriptorSize);
    mCommandList->SetGraphicsRootDescriptorTable(0, cbv);
    mCommandList->DrawIndexedInstanced(
        mBoxGeo->DrawArgs["pyramid"].IndexCount,
        1, mBoxGeo->DrawArgs["pyramid"].StartIndexLocation, mBoxGeo->DrawArgs["pyramid"].BaseVertexLocation, 0);
...
}

 

9. 상자 예제를 후면 선별 대신 전면 선별(D3D12_CULL_FRONT)를 사용하고, 선별 설정의 차이가 나타나도록, 와이어프레임 모드로 렌더링하라.

파이프 라인 상태 객체의 레스터화기 상태를 수정하면 된다. 

    CD3DX12_RASTERIZER_DESC rast(D3D12_DEFAULT);
    rast.FillMode = D3D12_FILL_MODE_WIREFRAME;
    rast.CullMode = D3D12_CULL_MODE_FRONT;
    psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(rast);

 

12. 상자 예제에서 뷰포트를 후면 버퍼의 왼쪽 절반으로 설정하라.

OnResize 함수에서 뷰포트의 너비를 조절할 수 있다. 

	mScreenViewport.Width    = static_cast<float>(mClientWidth / 2);

 

 

13. 상자 예제에 가위 판정을 도입해서, 후면 버퍼 중심에 놓인 너비 mClientWidth / 2, 높이 mClientHeight / 2 직사각형 외부에 있는 모든 픽셸을 폐기하라. 

OnResize 함수에서 가위 직사각형을 설정하면 된다. 

mScissorRect = { 0, 0, mClientWidth / 2, mClientHeight / 2 };

 

14. 색상이 시간에 따라 애니메이션되도록 수정하라. 매끄러운 가감속 함수(smooth easing function)를 이용할 것.

smoothstep이 HLSL의 내장 함수라고 한다. 

float4 PS(VertexOut pin) : SV_Target
{
	pin.Color.z = smoothstep(0, 1, cos(gTime));

    return pin.Color;
}

 

15. 픽셀 셰이더에서 Clip을 사용해보자.

float4 PS(VertexOut pin) : SV_Target
{
	// 인자 값이 0보다 작으면 폐기한다.
	clip(pin.Color.r - 0.5f);

    return pin.Color;
}

clip(x)는 x가 0보다 작다면 그 픽셀을 폐기한다. 픽셀 셰이더 내에서만 사용가능하다.

 

16. 상자 예제를, 픽셀 셰이더가 보간된 정점 색상과 상수 버퍼를 통해 주어진 gPulseColor 색상 사이에 매끄럽게 진동하는 색상을 출력하도록 수정하라.

상수 버퍼를 통해 gPulseColor를 전달해야 하므로 ObjectConstants 구조체를 수정해야 한다. 

struct ObjectConstants
{
    XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
    XMFLOAT4 PulseColor;
    float Time;
};

그리고 Update 함수에서 구조체를 채울 때, 임의의 색상으로 채운다. 

objConstants.PulseColor = XMFLOAT4(Colors::Aquamarine);

픽셀 셰이더를 문제에서 주어진 것처럼 수정한다. 

float4 PS(VertexOut pin) : SV_Target
{
	const float pi = 3.14159;

	// 사인 함수를 이용해서, 시간에 따라 [0, 1] 구간에서 진동하는 값을 구한다.
	float s = 0.5f * sin(2 * gTime - 0.25 * pi) + 0.5f;
	
	// 매개변수 s에 기초해서 pin.Color와 gPulseColor 사이를 매끄럽게 보간한 값을 구한다.
	float4 c = lerp(pin.Color, gPulseColor, s);

	return c;
}