해당 포스트는 DirectX12를 이용한 3D 게임 프로그래밍 입문 서적에 출제되는
각 챕터의 연습문제들에 대한 저의 풀이입니다.
문제가 간단하거나 설명 또는 증명과 같은 방식의 문제 또는 제가 해결하지 못한 문제들은 스킵하였습니다.
연습문제 1
해당 문제는 문제 지문 대로 정점 구조체 수정 후 D3D12_INPUT_ELEMENT_DESC 배열만 각각 알맞게 작성을 해주면 됩니다.
연습문제 2
상자 예저('Box')를 정점 버퍼 두 개 (그리고 입력 슬롯 두 개)를 사용해서 파이프라인에 정점들을 공급하도록 수정하라.
정점 버퍼 하나는 정점의 위치 성분을, 다른 하나는 색상 성분을 담으면 된다. 이를 위해 정점 구조체를 다음 두 구조체로 분할해야 할 것이다.
struct VPosData
{
XMFLOAT3 Pos;
}
struct VColorData
{
XMFLOAT3 Color;
}
그리고 D3D12_INPUT_ELEMENT_DESC 배열은 다음과 같은 모습이 되어야 할 것이다.
D3D12_INPUT_ELEMENT_DESC vertexDesc[] =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
D3D12_INPUT_PER_VERTEX_DATA, 0},
{"COLOR", 1, DXGI_FORMAT_R32G32B32_FLOAT, 1, 0,
D3D12_INPUT_PER_VERTEX_DATA, 0}
};
우선 지문의 지시대로 정점 구조체와 D3D12_INPUT_ELEMENT_DESC를 수정합니다.
각각의 데이터(위치, 색상)는 다른 입력 슬롯에 배치되기에 D3D12_INPUT_ELEMENT_DESC에서
AlignedByteOffset는 0으로 설정해야 합니다.
그다음 Draw 함수에서 ID3D12GraphicsCommandList::IASetVertexBuffers()를 이용해 각각 바인딩해줍니다.
mCommandList->IASetVertexBuffers(0, 1, &mBoxGeo->VertexBufferView());
mCommandList->IASetVertexBuffers(1, 1, &mBoxGe->VertexBufferView());
또한 위치 정보를 담는 정점과 색상 정보를 담는 정점 2개로 나누었기에
BuildBoxGeometry() 함수도 조금의 수정을 거쳐줍니다.
std::array<VPosData, 8> PosVertices =
{
VPosData({ XMFLOAT3(-1.0f, -1.0f, -1.0f) }),
...
...
}
std::array<VPosData, 8> ColorVertices =
{
VColorData({ XMFLOAT4(Colors::White) }),
...
...
}
그 후 인덱스 버퍼 생성과 함께 위치 정점 버퍼, 색상 정점 버퍼들을 생성해줍니다.
연습문제 3
도형의 위상 구조를 변경해보는 문제입니다.
간단하게 Draw() 함수에서 mCommandList->IASetPrimitiveTopology(위상 구조 열거형 멤버);를 이용해 각각 여러 위상 구조들을 설정해보고 각각의 실행해보면 됩니다.
//점목록
mCommandList->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST);
//선 띠
mCommandList->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP);
//선목록
mCommandList->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST);
//삼각형 띠
mCommandList->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
//삼각형 목록
mCommandList->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
연습문제 4
피라미드의 정점 목록과 색인 목록을 구축해서 그려보라.
상자의 정점 목록과 색인 목록을 조금 수정하여 해결하였습니다.
필요한 정점은 아래 4개 + 꼭짓점 1개 총 5개이고, 색인은 옆면 삼각형 4개와 밑면에 삼각형 2개
(정사각형을 2개의 삼각형으로 구축하기에) 총 6 x 3 = 18 개가 필요합니다.
이를 참고하여 코드를 작성해줍니다.
std::array<pyramidVertex, 5> vertices =
{
pyramidVertex({ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::BurlyWood) }),
pyramidVertex({ XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::BlueViolet) }),
pyramidVertex({ XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::CornflowerBlue) }),
pyramidVertex({ XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Honeydew) }),
pyramidVertex({ XMFLOAT3(0.0f, 1.0f, 0.0f), XMFLOAT4(Colors::Aquamarine) })
};
std::array<std::uint16_t, 18> indices =
{
//front face
0, 4, 1,
//back face
2, 3, 4,
//left face
2, 4, 0,
//right face
1, 4, 3,
//bottom face
2, 0, 1,
2, 1, 3
};
문제에서는 아래 정점은 초록색 위 정점은 빨간색으로 지정하라 하였지만 좀 더 이쁜 색들을 찾고자 여러 색들을 적용해보았습니다 ;)
연습문제 6
정점 셰이더에서 정점을 세계 공간으로 변환하기 전에 다음과 같은 변환을 적용하도록 수정하라.
vin.PosL.xy += 0.5f*sin(vinL.Pos.x)*sin(3.0f*gTime);
vin.PosL.z *= 0.6f + 0.4f*sin(2.0f*gTime);
문제 지문대로 정점 셰이더를 수정하고 gTime를 추가하기 위해 메인 cpp에서 ObjectConstants에 시간 정보를 담을 Time 변수를 추가하고 update() 함수에서 objConstants.Time = gt.TotalTime();를 추가하여 셰이더 코드에 시간에 대한 상수 버퍼를 추가해 사용할 수 있도록 해줍니다.
이와 같이 수정한다면 정육면체가 다음과 같이 일그러지는 애니메이션 효과를 보실 수 있습니다.
연습문제 7
상자의 정점들과 피라미드의 정점들을 하나의 큰 정점 버퍼, 색인 버퍼로 병합한다.
그런 다음 ID3D12CommandList::DrawIndexedInstanced의 매개변수를 적절히 설정해서 상자와 피라미드를 하나씩 그린다. 세계 변환 행렬을 적절히 사용해서, 세계 공간에서 상자와 피라미드가 겹쳐 있지 않게 해야 한다.
우선 피라미드와 상자의 정점, 색인 버퍼를 하나로 합치기 위하여 기존 BuildBoxGeometry() 함수를 수정해줍니다.
단순히 정점들과 색인들을 하나의 배열에 담기만 하면 되니 어렵진 않습니다.
std::array<Vertex, 13> Vertices =
{
Vertex({ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::White) }),
Vertex({ XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Black) }),
Vertex({ XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Red) }),
Vertex({ XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::Green) }),
Vertex({ XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Blue) }),
Vertex({ XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Yellow) }),
Vertex({ XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Cyan) }),
Vertex({ XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Magenta) }),
Vertex({ XMFLOAT3(0.0f, 2.0f, 0.0f), XMFLOAT4(Colors::Aquamarine) }),
Vertex({ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::White) }),
Vertex({ XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::RosyBrown) }),
Vertex({ XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::ForestGreen) }),
Vertex({ XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Black) })
};
std::array<std::uint16_t, 54> indices =
{
//box index
// 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,
//pyramid
//front face
0, 2, 1,
//back face
0, 3, 2,
//left face
0, 4, 3,
//right face
0, 1, 4,
//bottom face
1, 3, 2,
1, 4, 3
};
그 후 MeshGeometry 구조체의 버퍼 관련 자료들을 설정해주고 SubMeshGeometry 구조체 변수를 2개 생성해줍니다.
(하나는 박스, 하나는 피라미드용)
생성 이후 이름은 적절히 지어주고 내부 멤버들을 알맞게 지정해줍니다.
각 멤버의 설명들은 책을 참고해주세요.
SubmeshGeometry submesh_box;
submesh_box.IndexCount = 36;
submesh_box.StartIndexLocation = 0;
submesh_box.BaseVertexLocation = 0;
mBox_PyramidGeo->DrawArgs["box"] = submesh_box;
SubmeshGeometry submesh_pyramid;
submesh_pyramid.IndexCount = 18;
submesh_pyramid.StartIndexLocation = 36;
submesh_pyramid.BaseVertexLocation = 8;
mBox_PyramidGeo->DrawArgs["pyr"] = submesh_pyramid;
이제 정점과 색인들을 하나로 병합하였습니다.
이제 상자와 피라미드를 각각 그려봅시다.
Draw() 함수에서 DrawIndexedInstanced를 통해 상자와 피라미드를 그려줍니다.
mCommandList->SetGraphicsRootDescriptorTable(0, mCbvHeap->GetGPUDescriptorHandleForHeapStart());
mCommandList->DrawIndexedInstanced(
mBox_PyramidGeo->DrawArgs["box"].IndexCount,
1, mBox_PyramidGeo->DrawArgs["box"].StartIndexLocation, mBox_PyramidGeo->DrawArgs["box"].BaseVertexLocation, 0);
mCommandList->DrawIndexedInstanced(
mBox_PyramidGeo->DrawArgs["pyr"].IndexCount,
1, mBox_PyramidGeo->DrawArgs["pyr"].StartIndexLocation, mBox_PyramidGeo->DrawArgs["pyr"].BaseVertexLocation, 0);
다음과 같이 두 개의 도형이 생성되었습니다.
하나 두 도형이 하나의 위치로 합쳐져 있고 문제에서는 둘을 다른 위치에 배치하라 하였으니 좀 더 코드를 수정해줍니다.
저는 다음 방법으로 해결했습니다.
먼저 정점들은 상수 버퍼에 담긴 월드 행렬들에 의해 정점들의 위치가 계산됩니다.
그러므로 상수 버퍼를 2개 생성하여 각 상수 버퍼에 담길 월드 행렬에 약간의 수정을 거쳐 각 도형의 위치를 조정해주었습니다.
우선 BuildConstantBuffers() 함수에서 상수 버퍼를 하나 더 생성해줍니다.
void BoxApp::BuildConstantBuffers()
{
//2번째 인수(elementCount) 생성할 상수 버퍼는 2개이므로 2 지정
//uploadBuffer를 상수 버퍼에 사용할 때는 3번째 인수(isConstantBuffer)를 true지정
mObjectCB = std::make_unique<UploadBuffer<ObjectConstants>>(md3dDevice.Get(), 2, true);
UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
D3D12_GPU_VIRTUAL_ADDRESS cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress();
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;
auto handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(mCbvHeap->GetCPUDescriptorHandleForHeapStart());
handle.Offset(boxCBufIndex, mCbvSrvUavDescriptorSize);
md3dDevice->CreateConstantBufferView(
&cbvDesc,
handle);
}
그다음 상수 버퍼를 하나 더 생성해주었으니 서술자 힙 생성(BuildDescriptorHeaps())에서 NumDescriptors을 2로 지정해줍니다.
void BoxApp::BuildDescriptorHeaps()
{
D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
cbvHeapDesc.NumDescriptors = 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_GPU_DESCRIPTOR_HANDLE cbv1(mCbvHeap->GetGPUDescriptorHandleForHeapStart());
cbv1.Offset(0, mCbvSrvUavDescriptorSize);
mCommandList->SetGraphicsRootDescriptorTable(0, cbv1);
mCommandList->DrawIndexedInstanced(
mBox_PyramidGeo->DrawArgs["box"].IndexCount,
1, mBox_PyramidGeo->DrawArgs["box"].StartIndexLocation, mBox_PyramidGeo->DrawArgs["box"].BaseVertexLocation, 0);
CD3DX12_GPU_DESCRIPTOR_HANDLE cbv2(mCbvHeap->GetGPUDescriptorHandleForHeapStart());
cbv2.Offset(1, mCbvSrvUavDescriptorSize);
mCommandList->SetGraphicsRootDescriptorTable(0, cbv2);
mCommandList->DrawIndexedInstanced(
mBox_PyramidGeo->DrawArgs["pyr"].IndexCount,
1, mBox_PyramidGeo->DrawArgs["pyr"].StartIndexLocation, mBox_PyramidGeo->DrawArgs["pyr"].BaseVertexLocation, 0);
마지막으로 Update()에서 세계 변환 행렬을 수정해줍니다.
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);
// Build the view matrix.
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);
// 임시로 사용할 tempWorld
XMFLOAT4X4 tempWorld = MathHelper::Identity4x4();
// 행렬의 M_41 부분을 수정
tempWorld._41 = -1.2f;
XMMATRIX world = XMLoadFloat4x4(&tempWorld);
XMMATRIX proj = XMLoadFloat4x4(&mProj);
XMMATRIX worldViewProj = world * view * proj;
// Update the constant buffer with the latest worldViewProj matrix.
// 첫번째 objConstants 갱신
ObjectConstants objConstants;
XMStoreFloat4x4(&objConstants.WorldViewProj, XMMatrixTranspose(worldViewProj));
//objConstants.Time = gt.TotalTime();
//objConstants.pColor = XMFLOAT4(Colors::Aquamarine);
mObjectCB->CopyData(0, objConstants);
// 두번째 objConstants 갱신
tempWorld._41 = 1.2f;
world = XMLoadFloat4x4(&tempWorld);
worldViewProj = world * view*proj;
XMStoreFloat4x4(&objConstants.WorldViewProj, XMMatrixTranspose(worldViewProj));
mObjectCB->CopyData(1, objConstants);
}
행렬의 41을 수정하는 이유는 이동 변환 행렬에 대해 공부하셨다면 알고 계실 겁니다.
이렇게 해서 컴파일해보면 각각의 오브젝트들이 겹치지 않게 배치되었음을 볼 수 있습니다.
'Kua Devlog > DirectX12' 카테고리의 다른 글
DX12 3D 게임 프로그래밍 입문 연습문제 6장-2 (0) | 2020.09.02 |
---|