다음을 통해 공유


간접 그리기 및 GPU 선별(culling)

D3D12ExecuteIndirect 샘플은 간접 명령을 사용하여 콘텐츠를 그리는 방법을 보여 줍니다. 또한 이러한 명령을 실행하기 전에 컴퓨팅 셰이더의 GPU에서 조작하는 방법도 보여 줍니다.

이 샘플은 1024 그리기 호출을 설명하는 명령 버퍼를 만듭니다. 각 그리기 호출은 임의의 색, 위치 및 속도를 사용하여 삼각형을 렌더링합니다. 삼각형이 화면 너머까지 끝없이 애니메이트됩니다. 이 샘플에는 두 가지 모드가 있습니다. 첫 번째 모드에서 컴퓨팅 셰이더는 간접 명령을 검사하고, 실행해야 하는 명령을 설명하는 UAV(순서가 지정되지 않은 액세스 뷰)에 해당 명령을 추가할지 여부를 결정합니다. 두 번째 모드에서 모든 명령이 단순히 실행됩니다. 스페이스바를 눌러 모드 간을 전환합니다.

간접 명령 정의

먼저 간접 명령의 모양을 정의합니다. 이 샘플에서 실행하려는 명령은 다음을 수행해야 합니다.

1. CBV(상수 버퍼 뷰)를 업데이트합니다. 2. 삼각형을 그립니다.

이러한 그리기 명령은 D3D12ExecuteIndirect 클래스 정의의 다음 구조로 나타냅니다. 명령은 이 구조에 정의된 순서대로 순차적으로 실행됩니다.

  
// Data structure to match the command signature used for ExecuteIndirect.
struct IndirectCommand
{
       D3D12_GPU_VIRTUAL_ADDRESS cbv;
       D3D12_DRAW_ARGUMENTS drawArguments;
};
호출 흐름 매개 변수
D3D12_GPU_VIRTUAL_ADDRESS(단순히 UINT64)
D3D12_DRAW_ARGUMENTS

 

이 데이터 구조와 함께 사용하기 위해, ExecuteIndirect API에 전달되는 데이터를 해석하는 방법을 GPU에 알려주는 명령 서명도 만들어집니다. 이 명령 서명과 다음 코드 대부분이 LoadAssets 메서드에 추가됩니다.

// Create the command signature used for indirect drawing.
{
       // Each command consists of a CBV update and a DrawInstanced call.
       D3D12_INDIRECT_ARGUMENT_DESC argumentDescs[2] = {};
       argumentDescs[0].Type = D3D12_INDIRECT_ARGUMENT_TYPE_CONSTANT_BUFFER_VIEW;
       argumentDescs[0].ConstantBufferView.RootParameterIndex = Cbv;
       argumentDescs[1].Type = D3D12_INDIRECT_ARGUMENT_TYPE_DRAW;

       D3D12_COMMAND_SIGNATURE_DESC commandSignatureDesc = {};
       commandSignatureDesc.pArgumentDescs = argumentDescs;
       commandSignatureDesc.NumArgumentDescs = _countof(argumentDescs);
       commandSignatureDesc.ByteStride = sizeof(IndirectCommand);

       ThrowIfFailed(m_device->CreateCommandSignature(&commandSignatureDesc, m_rootSignature.Get(), IID_PPV_ARGS(&m_commandSignature)));
}
호출 흐름 매개 변수
D3D12_INDIRECT_ARGUMENT_DESC D3D12_INDIRECT_ARGUMENT_TYPE
D3D12_COMMAND_SIGNATURE_DESC
CreateCommandSignature

 

그래픽 및 컴퓨팅 루트 서명 만들기

그래픽 및 컴퓨팅 루트 서명도 만듭니다. 그래픽 루트 서명은 루트 CBV만 정의합니다. 명령 서명이 정의되면 이 루트 매개 변수의 인덱스가 D3D12_INDIRECT_ARGUMENT_DESC 매핑됩니다(위에 표시됨). 컴퓨팅 루트 서명은 다음을 정의합니다.

  • 3개의 슬롯(SRV 2개 및 UAV 1개)이 있는 일반적인 설명자 테이블:
    • SRV 1개는 컴퓨팅 셰이더에 상수 버퍼를 노출합니다.
    • SRV 1개는 컴퓨팅 셰이더에 명령 버퍼를 노출합니다.
    • UAV에서 컴퓨팅 셰이더는 보이는 삼각형에 대한 명령을 저장합니다.
  • 루트 상수 4개:
    • 삼각형에서 한 모서리 너비의 절반
    • 삼각형 꼭짓점의 z 위치
    • 동종 공간에서 컬링 평면의 +/- x 오프셋 [-1,1]
    • 명령 버퍼에 간접 명령 수
// Create the root signatures.
{
       CD3DX12_ROOT_PARAMETER rootParameters[GraphicsRootParametersCount];
       rootParameters[Cbv].InitAsConstantBufferView(0, 0, D3D12_SHADER_VISIBILITY_VERTEX);

       CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
       rootSignatureDesc.Init(_countof(rootParameters), rootParameters, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

       ComPtr<ID3DBlob> signature;
       ComPtr<ID3DBlob> error;
       ThrowIfFailed(D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error));
       ThrowIfFailed(m_device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&m_rootSignature)));

       // Create compute signature.
       CD3DX12_DESCRIPTOR_RANGE ranges[2];
       ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 2, 0);
       ranges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0);

       CD3DX12_ROOT_PARAMETER computeRootParameters[ComputeRootParametersCount];
       computeRootParameters[SrvUavTable].InitAsDescriptorTable(2, ranges);
       computeRootParameters[RootConstants].InitAsConstants(4, 0);

       CD3DX12_ROOT_SIGNATURE_DESC computeRootSignatureDesc;
       computeRootSignatureDesc.Init(_countof(computeRootParameters), computeRootParameters);

       ThrowIfFailed(D3D12SerializeRootSignature(&computeRootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error));
       ThrowIfFailed(m_device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&m_computeRootSignature)));
}
호출 흐름 매개 변수
CD3DX12_ROOT_PARAMETER D3D12_SHADER_VISIBILITY
CD3DX12_ROOT_SIGNATURE_DESC D3D12_ROOT_SIGNATURE_FLAGS
ID3DBlob
D3D12SerializeRootSignature D3D_ROOT_SIGNATURE_VERSION
CreateRootSignature
CD3DX12_DESCRIPTOR_RANGE D3D12_DESCRIPTOR_RANGE_TYPE
CD3DX12_ROOT_PARAMETER D3D12_SHADER_VISIBILITY
CD3DX12_ROOT_SIGNATURE_DESC D3D12_ROOT_SIGNATURE_FLAGS
ID3DBlob
D3D12SerializeRootSignature D3D_ROOT_SIGNATURE_VERSION
CreateRootSignature

 

컴퓨팅 셰이더의 SRV(셰이더 리소스 뷰) 만들기

샘플은 파이프라인 상태 개체, 꼭짓점 버퍼, 깊이 스텐실 및 상수 버퍼를 만든 후 컴퓨팅 셰이더가 상수 버퍼의 데이터에 액세스할 수 있도록 상수 버퍼의 SRV(셰이더 리소스 뷰)를 만듭니다.

// Create shader resource views (SRV) of the constant buffers for the
// compute shader to read from.
       D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
       srvDesc.Format = DXGI_FORMAT_UNKNOWN;
       srvDesc.ViewDimension = D3D12_SRV_DIMENSION_BUFFER;
       srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
       srvDesc.Buffer.NumElements = TriangleCount;
       srvDesc.Buffer.StructureByteStride = sizeof(ConstantBufferData);
       srvDesc.Buffer.Flags = D3D12_BUFFER_SRV_FLAG_NONE;

       CD3DX12_CPU_DESCRIPTOR_HANDLE cbvSrvHandle(m_cbvSrvUavHeap->GetCPUDescriptorHandleForHeapStart(), CbvSrvOffset, m_cbvSrvUavDescriptorSize);
       for (UINT frame = 0; frame < FrameCount; frame++)
       {
              srvDesc.Buffer.FirstElement = frame * TriangleCount;
              m_device->CreateShaderResourceView(m_constantBuffer.Get(), &srvDesc, cbvSrvHandle);
              cbvSrvHandle.Offset(CbvSrvUavDescriptorCountPerFrame, m_cbvSrvUavDescriptorSize);
       }
호출 흐름 매개 변수
D3D12_SHADER_RESOURCE_VIEW_DESC
DXGI_FORMAT
D3D12_SRV_DIMENSION
D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING
CD3DX12_CPU_DESCRIPTOR_HANDLE GetCPUDescriptorHandleForHeapStart
CreateShaderResourceView

 

간접 명령 버퍼 만들기

그런 다음, 간접 명령 버퍼를 만들고 다음 코드를 사용하여 해당 콘텐츠를 정의합니다. 동일한 삼각형 꼭짓점을 1024번 그리지만, 그리기 호출마다 다른 상수 버퍼 위치를 가리킵니다.

       D3D12_GPU_VIRTUAL_ADDRESS gpuAddress = m_constantBuffer->GetGPUVirtualAddress();
       UINT commandIndex = 0;

       for (UINT frame = 0; frame < FrameCount; frame++)
       {
              for (UINT n = 0; n < TriangleCount; n++)
              {
                    commands[commandIndex].cbv = gpuAddress;
                    commands[commandIndex].drawArguments.VertexCountPerInstance = 3;
                    commands[commandIndex].drawArguments.InstanceCount = 1;
                    commands[commandIndex].drawArguments.StartVertexLocation = 0;
                    commands[commandIndex].drawArguments.StartInstanceLocation = 0;

                    commandIndex++;
                    gpuAddress += sizeof(ConstantBufferData);
              }
       }
호출 흐름 매개 변수
D3D12_GPU_VIRTUAL_ADDRESS GetGPUVirtualAddress

 

또한 명령 버퍼를 GPU에 업로드한 후, 컴퓨팅 셰이더에서 읽을 SRv를 만듭니다. 이것은 상수 버퍼에서 만든 SRV와 매우 비슷합니다.

// Create SRVs for the command buffers.
       D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
       srvDesc.Format = DXGI_FORMAT_UNKNOWN;
       srvDesc.ViewDimension = D3D12_SRV_DIMENSION_BUFFER;
       srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
       srvDesc.Buffer.NumElements = TriangleCount;
       srvDesc.Buffer.StructureByteStride = sizeof(IndirectCommand);
       srvDesc.Buffer.Flags = D3D12_BUFFER_SRV_FLAG_NONE;

       CD3DX12_CPU_DESCRIPTOR_HANDLE commandsHandle(m_cbvSrvUavHeap->GetCPUDescriptorHandleForHeapStart(), CommandsOffset, m_cbvSrvUavDescriptorSize);
       for (UINT frame = 0; frame < FrameCount; frame++)
       {
              srvDesc.Buffer.FirstElement = frame * TriangleCount;
              m_device->CreateShaderResourceView(m_commandBuffer.Get(), &srvDesc, commandsHandle);
              commandsHandle.Offset(CbvSrvUavDescriptorCountPerFrame, m_cbvSrvUavDescriptorSize);
       }
호출 흐름 매개 변수
D3D12_SHADER_RESOURCE_VIEW_DESC
DXGI_FORMAT
D3D12_SRV_DIMENSION
D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING
D3D12_BUFFER_SRV_FLAG
CD3DX12_CPU_DESCRIPTOR_HANDLE GetCPUDescriptorHandleForHeapStart
CreateShaderResourceView

 

컴퓨팅 UAV 만들기

컴퓨팅 작업의 결과를 저장할 UAV를 만들어야 합니다. 컴퓨팅 셰이더에서 삼각형이 렌더링 대상에 표시되어야 할 경우 이 UAV에 추가된 후 ExecuteIndirect API에서 사용됩니다.

CD3DX12_CPU_DESCRIPTOR_HANDLE processedCommandsHandle(m_cbvSrvUavHeap->GetCPUDescriptorHandleForHeapStart(), ProcessedCommandsOffset, m_cbvSrvUavDescriptorSize);
for (UINT frame = 0; frame < FrameCount; frame++)
{
       // Allocate a buffer large enough to hold all of the indirect commands
       // for a single frame as well as a UAV counter.
       commandBufferDesc = CD3DX12_RESOURCE_DESC::Buffer(CommandBufferSizePerFrame + sizeof(UINT), D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS);
       CD3DX12_HEAP_PROPERTIES heapProps(D3D12_HEAP_TYPE_DEFAULT);
       ThrowIfFailed(m_device->CreateCommittedResource(
             &heapProps,
             D3D12_HEAP_FLAG_NONE,
             &commandBufferDesc,
             D3D12_RESOURCE_STATE_COPY_DEST,
             nullptr,
             IID_PPV_ARGS(&m_processedCommandBuffers[frame])));

       D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc = {};
       uavDesc.Format = DXGI_FORMAT_UNKNOWN;
       uavDesc.ViewDimension = D3D12_UAV_DIMENSION_BUFFER;
       uavDesc.Buffer.FirstElement = 0;
       uavDesc.Buffer.NumElements = TriangleCount;
       uavDesc.Buffer.StructureByteStride = sizeof(IndirectCommand);
       uavDesc.Buffer.CounterOffsetInBytes = CommandBufferSizePerFrame;
       uavDesc.Buffer.Flags = D3D12_BUFFER_UAV_FLAG_NONE;

       m_device->CreateUnorderedAccessView(
             m_processedCommandBuffers[frame].Get(),
             m_processedCommandBuffers[frame].Get(),
             &uavDesc,
             processedCommandsHandle);

       processedCommandsHandle.Offset(CbvSrvUavDescriptorCountPerFrame, m_cbvSrvUavDescriptorSize);
}
호출 흐름 매개 변수
CD3DX12_CPU_DESCRIPTOR_HANDLE GetCPUDescriptorHandleForHeapStart
CD3DX12_RESOURCE_DESC D3D12_RESOURCE_FLAGS
CreateCommittedResource
CD3DX12_HEAP_PROPERTIES
D3D12_HEAP_TYPE
D3D12_HEAP_FLAG
D3D12_RESOURCE_STATES
D3D12_UNORDERED_ACCESS_VIEW_DESC
DXGI_FORMAT
D3D12_UAV_DIMENSION
D3D12_BUFFER_UAV_FLAGS
CreateUnorderedAccessView

 

프레임 그리기

프레임을 그리려는 경우, 컴퓨팅 셰이더가 호출되고 GPU에서 간접 명령이 처리되는 모드인 경우 먼저 해당 작업을 디스패치하여 ExecuteIndirect의 명령 버퍼를 채웁니다. 다음 코드 조각이 PopulateCommandLists 메서드에 추가됩니다.

// Record the compute commands that will cull triangles and prevent them from being processed by the vertex shader.
if (m_enableCulling)
{
       UINT frameDescriptorOffset = m_frameIndex * CbvSrvUavDescriptorCountPerFrame;
       D3D12_GPU_DESCRIPTOR_HANDLE cbvSrvUavHandle = m_cbvSrvUavHeap->GetGPUDescriptorHandleForHeapStart();

       m_computeCommandList->SetComputeRootSignature(m_computeRootSignature.Get());

       ID3D12DescriptorHeap* ppHeaps[] = { m_cbvSrvUavHeap.Get() };
       m_computeCommandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);

       m_computeCommandList->SetComputeRootDescriptorTable(
              SrvUavTable,
              CD3DX12_GPU_DESCRIPTOR_HANDLE(cbvSrvUavHandle, CbvSrvOffset + frameDescriptorOffset, m_cbvSrvUavDescriptorSize));

       m_computeCommandList->SetComputeRoot32BitConstants(RootConstants, 4, reinterpret_cast<void*>(&m_csRootConstants), 0);

       // Reset the UAV counter for this frame.
       m_computeCommandList->CopyBufferRegion(m_processedCommandBuffers[m_frameIndex].Get(), CommandBufferSizePerFrame, m_processedCommandBufferCounterReset.Get(), 0, sizeof(UINT));

       D3D12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition(m_processedCommandBuffers[m_frameIndex].Get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
       m_computeCommandList->ResourceBarrier(1, &barrier);

       m_computeCommandList->Dispatch(static_cast<UINT>(ceil(TriangleCount / float(ComputeThreadBlockSize))), 1, 1);
}

ThrowIfFailed(m_computeCommandList->Close());
호출 흐름 매개 변수
D3D12_GPU_DESCRIPTOR_HANDLE GetGPUDescriptorHandleForHeapStart
SetComputeRootSignature
ID3D12DescriptorHeap
SetDescriptorHeaps
SetComputeRootDescriptorTable CD3DX12_GPU_DESCRIPTOR_HANDLE
SetComputeRoot32BitConstants
CopyBufferRegion
D3D12_RESOURCE_BARRIER
CD3DX12_RESOURCE_BARRIER
D3D12_RESOURCE_STATES
ResourceBarrier
Dispatch
닫기

 

그런 후 UAV[GPU 선별(culling) 사용] 또는 전체 명령 버퍼[GPU 선별(culling) 사용 안 함]에서 명령을 실행합니다.

// Record the rendering commands.
{
       // Set necessary state.
       m_commandList->SetGraphicsRootSignature(m_rootSignature.Get());

       ID3D12DescriptorHeap* ppHeaps[] = { m_cbvSrvUavHeap.Get() };
       m_commandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);

       m_commandList->RSSetViewports(1, &m_viewport);
       m_commandList->RSSetScissorRects(1, m_enableCulling ? &m_cullingScissorRect : &m_scissorRect);

       // Indicate that the command buffer will be used for indirect drawing
       // and that the back buffer will be used as a render target.
       D3D12_RESOURCE_BARRIER barriers[2] = {
              CD3DX12_RESOURCE_BARRIER::Transition(
                    m_enableCulling ? m_processedCommandBuffers[m_frameIndex].Get() : m_commandBuffer.Get(),
                    m_enableCulling ? D3D12_RESOURCE_STATE_UNORDERED_ACCESS : D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE,
                    D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT),
              CD3DX12_RESOURCE_BARRIER::Transition(
                    m_renderTargets[m_frameIndex].Get(),
                    D3D12_RESOURCE_STATE_PRESENT,
                    D3D12_RESOURCE_STATE_RENDER_TARGET)
       };

       m_commandList->ResourceBarrier(_countof(barriers), barriers);

       CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart(), m_frameIndex, m_rtvDescriptorSize);
       CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle(m_dsvHeap->GetCPUDescriptorHandleForHeapStart());
       m_commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, &dsvHandle);

       // Record commands.
       const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
       m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
       m_commandList->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);

       m_commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
       m_commandList->IASetVertexBuffers(0, 1, &m_vertexBufferView);

       if (m_enableCulling)
       {
              // Draw the triangles that have not been culled.
              m_commandList->ExecuteIndirect(
                    m_commandSignature.Get(),
                    TriangleCount,
                    m_processedCommandBuffers[m_frameIndex].Get(),
                    0,
                    m_processedCommandBuffers[m_frameIndex].Get(),
                    CommandBufferSizePerFrame);
       }
       else
       {
              // Draw all of the triangles.
              m_commandList->ExecuteIndirect(
                    m_commandSignature.Get(),
                    TriangleCount,
                    m_commandBuffer.Get(),
                    CommandBufferSizePerFrame * m_frameIndex,
                    nullptr,
                    0);
       }

       // Indicate that the command buffer may be used by the compute shader
       // and that the back buffer will now be used to present.
       barriers[0].Transition.StateBefore = D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT;
       barriers[0].Transition.StateAfter = m_enableCulling ? D3D12_RESOURCE_STATE_COPY_DEST : D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE;
       barriers[1].Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
       barriers[1].Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;

       m_commandList->ResourceBarrier(_countof(barriers), barriers);

       ThrowIfFailed(m_commandList->Close());
}
호출 흐름 매개 변수
SetGraphicsRootSignature
ID3D12DescriptorHeap
SetDescriptorHeaps
RSSetViewports
RSSetScissorRects
D3D12_RESOURCE_BARRIER
CD3DX12_RESOURCE_BARRIER
D3D12_RESOURCE_STATES
ResourceBarrier
CD3DX12_CPU_DESCRIPTOR_HANDLE GetCPUDescriptorHandleForHeapStart
OMSetRenderTargets
ClearRenderTargetView
ClearDepthStencilView D3D12_CLEAR_FLAGS
IASetPrimitiveTopology D3D_PRIMITIVE_TOPOLOGY
IASetVertexBuffers
ExecuteIndirect
ResourceBarrier D3D12_RESOURCE_STATES
닫기

 

GPU 선별(culling) 모드인 경우, 그래픽 명령 큐가 간접 명령 실행을 시작하기 전에 컴퓨팅 작업이 완료될 때까지 대기하도록 합니다. OnRender 메서드에서 다음 코드 조각이 추가됩니다.

// Execute the compute work.
if (m_enableCulling)
{
       ID3D12CommandList* ppCommandLists[] = { m_computeCommandList.Get() };
       m_computeCommandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
       m_computeCommandQueue->Signal(m_computeFence.Get(), m_fenceValues[m_frameIndex]);

       // Execute the rendering work only when the compute work is complete.
       m_commandQueue->Wait(m_computeFence.Get(), m_fenceValues[m_frameIndex]);
}

// Execute the rendering work.
ID3D12CommandList* ppCommandLists[] = { m_commandList.Get() };
m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
호출 흐름 매개 변수
ID3D12CommandList
ExecuteCommandLists
Signal
연결 시도 간격
ID3D12CommandList
ExecuteCommandLists

 

샘플 실행

GPU 기본 형식 선별(culling)이 있는 샘플입니다.

GPU 선별(culling)이 있는 간접 샘플을 실행하는 스크린샷

GPU 기본 형식 선별(culling)이 없는 샘플입니다.

GPU 선별(culling)이 없는 간접 샘플을 실행하는 스크린샷

D3D12 코드 연습

DirectX 고급 학습 비디오 자습서: 간접 및 비동기 GPU 컬링 실행

간접 그리기