[DirectX 11] Hierarchical Z-Buffer Occlusion Culling

2022. 4. 4. 16:42·Graphics

Hardware Occlusion Culling 

1) 구현 원리

 - Occluder가 Rendering 된 상태에서 (Z-Buffer 유지 상태) Occludee를 Rendering 하여Pixel에 그려지는지 테스트 하는 방식.

 - Low-Poly Data나 Bounding Mesh를 그려서 Z-Buffer를 구성한다.

 - Occludee를Pixel Write를 하지 않고 Z-Test만 한다.

 - 그려지는 Pixel 수를 GPU측 메모리에 기록해둔다.

 - 해당 Occludee의Pixel 수를 GPU로부터 얻어온다.

2) 단점

 - DrawCall 회수를 줄이지 못한다.

 - 두번 그려야 하므로 왠만해선 빨라지기 힘들다.

- Occludee의크기가 커질수록 Z-Test 회수도 많아진다.

 - Pixel 수를 받아올때PCI버스를 타고GPU Memory -> System Memory로 가져오는 과정에서 비용이 많이든다.

 

Hierarchical Z-Buffer Occlusion Culling

1) 구현 원리

 - Occluder와 Occludee가 꼭 정밀할 필요는 없기에, 간단한 Bounding Sphere로 대체하여 비교한다.

 - Z-Buffer의 해상도 또한 정밀할 필요가 없다.

 - Bounding Sphere를 통해 Z-Test를 하므로 그리지 않고 Sphere의 한 점만 Projection하여 Depth 값을 비교한다.

 - 직접 버퍼에 그릴 필요가 없으므로 Pixel Shader가 아닌 Compute Shader를 활용하여 Depth 테스트를 한다.

 - Screen Space에서 Occludee를 중심점과 Occludee가 그려질 2x2 Texel의 Depth값을 비교한다.

 - Occludee의 크기가 크면 Z-Buffer의 해상도는 낮아도 되며, Occludee의 크기가 작으면 Z-Buffer의 해상도가 높아야한다.

 - Occludee가 Screen Space에서 2x2 Texel을 보장할 수 있다면 해당 해상도의 Z-Buffer 기준으로 비교한다.

 - 2x2 Texel을 기준으로 잡는 이유는 Screen Space 상에서 항상 Occludee가 그려지는 영역 전체를 커버할수 있기 때문이다.

 - 1x1 Texel을 기준으로 잡을 경우 Texel의 경계에 걸치게되면 잘못된 Depth를 비교하기 때문에 2x2 Texel을 기준으로 잡는다.

 - Occludee의 경계구가 Screen Space 상에서 반드시 2x2 Texel이 되도록 ceil(log2(texelSize)) 공식을 활용하여 MipMap을 선택하여 비교한다.

 - 적절한 MipMap 설정후 해당 Z-Buffer MipMap의 2x2 Texel을 Sampling하여 가장 큰 Depth 기준으로 비교를하여 Culling을 실행한다.

 

2) 장점

 - 낮은 해상도의 Z-Buffer를 사용하므로 Z-Buffer의 Read/Write 시간을 줄일 수 있다.

 - Compute Shader를 활용하여 Bounding Sphere당 스레드를 할당하여 Depth 테스트 병렬화를 할 수 있다.

 - 실질적으로 해당 Occludee를 비교하기 위해서 float4(CenterPosition, Radius)만 필요로 하므로 GPU 메모리를 크게 필요로 하지 않는다.

 

Deferred Context & CommandList

- DirectX 11 에서 지원하는 Deferred Context와 Command List를 활용하면 MipMap Sampling Rendering을 예약해둘 수 있다.

 1) 해당 CommandList를 예약하기 위한 Deferred Context를 생성한다.

 2) 생성한 Deferred Context를 통해 실질적인 MipMap Sampling 과정을 설정한다.

 3) 해당 CommandList를 보관해둔 후 사용한 Deferred Context는 해제해준다.

 4) Occluder Z-Buffer를 그린 후 미리 설정해둔 CommandList를 통해 MipMap Sampling을 실행한다.

 


ID3D11DeviceContext* context;
g_Device->CreateDeferredContext(0, &context);

context->RSSetViewports(1, m_Hiz_VP);
context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
context->IASetVertexBuffers(0, 1, m_Screen_DB->VertexBuf->GetAddress(), &m_Screen_DB->Stride, &m_Screen_DB->Offset);
context->IASetIndexBuffer(m_Screen_DB->IndexBuf->Get(), DXGI_FORMAT_R32_UINT, 0);

m_Screen_VS->Update(context);

// Draw Screen MipMap..
// 최대 크기의 Mip Map을 제외한 모든 Mip Map Sampling 실행..
for (UINT miplevel = 1; miplevel < hizDesc->MipLevels; miplevel++)
{
	context->OMSetRenderTargets(1, &m_MipMap_RTV[miplevel], nullptr);

	// 한단계 위의 Mip Level을 Resource로 설정..
	m_HizMipMap_PS->SetShaderResourceView<gLastMipMap>(m_MipMap_SRV[miplevel - 1]);

	m_HizMipMap_PS->Update(context);

	context->DrawIndexed(m_Screen_DB->IndexCount, 0, 0);
}

context->FinishCommandList(FALSE, &m_MipMap_CommandList);

// 예약 후 해당 DeviceContext는 해제..
context->Release();

- Deferred Context를 활용한 CommandList 설정 방식이다.

 

// Occluder Depth 그리기..
g_Context->RSSetViewports(1, m_Hiz_VP);
g_Context->RSSetState(m_NoCull_RS);
g_Context->OMSetRenderTargets(1, &m_MipMap_RTV[0], m_HizDepth_DSV);
g_Context->ClearRenderTargetView(m_MipMap_RTV[0], reinterpret_cast<const float*>(&DXColors::White));
g_Context->ClearDepthStencilView(m_HizDepth_DSV, D3D11_CLEAR_DEPTH, 1.0, 0);

g_Context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

VertexBuffer* originVertex = nullptr;
IndexBuffer* originIndex = nullptr;

ID3D11Buffer* convertVertex = nullptr;
ID3D11Buffer* convertIndex = nullptr;

// Occluder List Z-Buffer 설정..
for (MeshBuffer* occluder : g_GlobalData->OccluderList)
{
	// 해당 Occluder Vertex & Index Buffer..
	originVertex = occluder->VertexBuf;
	originIndex = occluder->IndexBuf;

	convertVertex = (ID3D11Buffer*)originVertex->pVertexBuf;
	convertIndex = (ID3D11Buffer*)originIndex->pIndexBuf;

	CB_DepthStaticMesh objectBuf;
	objectBuf.gWorldViewProj = viewproj;

	m_Depth_VS->ConstantBufferUpdate(&objectBuf);

	m_Depth_VS->Update();
	m_Depth_PS->Update();

	g_Context->IASetVertexBuffers(0, 1, &convertVertex, &originVertex->Stride, &originVertex->Offset);
	g_Context->IASetIndexBuffer(convertIndex, DXGI_FORMAT_R32_UINT, 0);

	g_Context->DrawIndexed(originIndex->Count, 0, 0);
}

// 예약해둔 Z MipMap Sampling 실행..
g_Context->ExecuteCommandList(m_MipMap_CommandList, FALSE);

- 설정해둔 CommandList를 통해 MipMap Sampling을 하는 과정이다.

 

 


Z-Buffer MipMap을 통한 Occlusion Culling 과정

Occludee 크기에 맞는 2x2 Texel을 보장하는 Z-Buffer MipMap 설정

 

 

Occludee Bounding Sphere NDC 공간으로 투영한 후 해당 2x2 Texel Sampling

 

// Occluder Z-Buffer를 기준으로 Occludee Occlusion Culling..
void CullingPass::OcclusionCullingQuery()
{
	if (CullingRenderMeshList.empty() || g_GlobalData->OccluderList.empty()) return;

	CameraData* cam = g_GlobalData->MainCamera_Data;

	Matrix& view = cam->CamView;
	Matrix& proj = cam->CamProj;
	Matrix& viewproj = cam->CamViewProj;

	// View Frustum 설정..
	m_Frustum.FrustumTransform(viewproj);

	// 실질적인 Render Count 초기화..
	m_RenderCount = 0;

	for (int i = 0; i < CullingRenderMeshList.size(); i++)
	{
		m_RenderData = CullingRenderMeshList[i];

		// 모든 오브젝트 Draw 초기화..
		m_RenderData->m_Draw = false;

		// 활성화 되있는 Object만 Culling..
		if (m_RenderData->m_ObjectData->IsActive == false) continue;

		// 해당 Collider Transform Update..
		m_Sphere = m_RenderData->m_Mesh->m_MeshProperty->BoundSphere;
		m_Sphere.Transform(m_Sphere, m_RenderData->m_ObjectData->World);

		// Collider XYZ (Center) W (Radius) 형태로 Buffer 삽입..
		m_ColliderData.x = m_Sphere.Center.x;
		m_ColliderData.y = m_Sphere.Center.y;
		m_ColliderData.z = m_Sphere.Center.z;
		m_ColliderData.w = m_Sphere.Radius;

		m_ColliderList[m_RenderCount++] = m_ColliderData;
	}

	// Mapping SubResource Data..
	D3D11_MAPPED_SUBRESOURCE mappedResource;
	ZeroMemory(&mappedResource, sizeof(D3D11_MAPPED_SUBRESOURCE));

	// GPU Access Lock Buffer Data..
	g_Context->Map(m_Collider_Buffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
	
	// Copy Resource Data..
	memcpy(mappedResource.pData, &m_ColliderList[0], sizeof(Vector4) * m_RenderCount);
	
	// GPU Access UnLock Buffer Data..
	g_Context->Unmap(m_Collider_Buffer, 0);

	// Culling Constant Buffer Update..
	CB_HizCull cullBuf;
	cullBuf.gView = view;
	cullBuf.gProj = proj;
	cullBuf.gViewProj = viewproj;
	cullBuf.gFrustumPlanes[0] = m_Frustum.Planes[0].point;
	cullBuf.gFrustumPlanes[1] = m_Frustum.Planes[1].point;
	cullBuf.gFrustumPlanes[2] = m_Frustum.Planes[2].point;
	cullBuf.gFrustumPlanes[3] = m_Frustum.Planes[3].point;
	cullBuf.gFrustumPlanes[4] = m_Frustum.Planes[4].point;
	cullBuf.gFrustumPlanes[5] = m_Frustum.Planes[5].point;
	cullBuf.gEyePos.x = cam->CamPos.x;
	cullBuf.gEyePos.y = cam->CamPos.y;
	cullBuf.gEyePos.z = cam->CamPos.z;
	cullBuf.gViewportSize = Vector2(m_Width, m_Height);

	m_HizCull_CS->ConstantBufferUpdate(&cullBuf);

	m_HizCull_CS->SetShaderResourceView<gHizMap>(m_Hiz_SRV);
	m_HizCull_CS->SetShaderResourceView<gColliderBuffer>(m_Collider_SRV);
	m_HizCull_CS->SetUnorderedAccessView<gCullingBuffer>(m_Culling_UAV);

	m_HizCull_CS->Update();

	g_Context->Dispatch(m_RenderCount, 1, 1);
}

// GPGPU을 통해 나온 결과값을 기반으로 Draw 상태 설정..
void CullingPass::DrawStateUpdate()
{
	if (CullingRenderMeshList.empty() || g_GlobalData->OccluderList.empty()) return;

	// GPU -> CPU Culling Result Data Copy
	g_Context->CopyResource(m_ResultCopy_Buffer, m_Culling_Buffer);

	// Mapping SubResource Data..
	D3D11_MAPPED_SUBRESOURCE mappedResource;
	ZeroMemory(&mappedResource, sizeof(D3D11_MAPPED_SUBRESOURCE));
	
	// GPU Access Lock Buffer Data..
	g_Context->Map(m_ResultCopy_Buffer, 0, D3D11_MAP_READ, 0, &mappedResource);

	// Copy Resource Data..
	memcpy(&m_ResultList[0], (float*)mappedResource.pData, sizeof(float) * m_RenderCount);

	// GPU Access UnLock Buffer Data..
	g_Context->Unmap(m_ResultCopy_Buffer, 0);

	// 실질적인 Render Count 초기화..
	m_RenderCount = 0;

	for (int i = 0; i < CullingRenderMeshList.size(); i++)
	{
		m_RenderData = CullingRenderMeshList[i];

		// 활성화 되있는 Object만 Culling..
		if (m_RenderData->m_ObjectData->IsActive == false) continue;

		// Culling 결과에 대한 Draw 상태 업데이트..
		m_RenderData->m_Draw = (bool)m_ResultList[m_RenderCount++];
	}
}

- Occluder Z-Buffer와 Occludee Bounding Sphere의 비교를 통해 GPGPU(Compute Shader) 측에서 결과 값을 내보낸 후 해당 결과값을 기반으로 Occludee들의 Draw 여부를 설정하여 Rendering을 실행한다.

 

적용 영상

Hierarchical Z-Buffer Occlusion Culling 적용 영상

 

저작자표시 (새창열림)

'Graphics' 카테고리의 다른 글

[DirectX 11] Rim Lighting  (0) 2022.04.25
[DirectX 11] Vertex Texture Fetch Skinning Animation Instancing  (0) 2022.04.21
[DirectX 11] Fast Picking (Object ID Picking)  (6) 2022.03.29
[DirectX 11] Game Engine Render Resource 동기화 방식 및 Resource 관리 방식  (0) 2022.03.16
[DirectX 11] Game Engine & Graphic Engine 분리 및 Render Data 전달 방식  (0) 2022.03.07
'Graphics' 카테고리의 다른 글
  • [DirectX 11] Rim Lighting
  • [DirectX 11] Vertex Texture Fetch Skinning Animation Instancing
  • [DirectX 11] Fast Picking (Object ID Picking)
  • [DirectX 11] Game Engine Render Resource 동기화 방식 및 Resource 관리 방식
KyuHwang
KyuHwang
  • KyuHwang
    DirectX Engine
    KyuHwang
  • 전체
    오늘
    어제
    • 분류 전체보기 (50)
      • C++ (4)
      • CS (0)
      • Graphics (32)
      • DLL (2)
      • Shader (7)
      • Project (4)
      • ETC (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • YouTube
  • 공지사항

  • 인기 글

  • 태그

    Define Macro
    Win API
    Order Independent Transparency
    mobile format
    rigging chain
    Implicit Linking
    std::is_base_of
    hlsl
    C ++
    texture block compression
    Shader Macro
    nvtt
    Hash Code
    animation constraint
    bc format
    animation retargeting
    DirectX 2D
    Alpha Blending
    Shader Resource 관리
    RunTime Compile Shader
    std::enable_if
    Directx11
    Explicit Linking
    dll
    DLL Interface
    Return Type Operator
    shader
    Define Function
    Project
    Shader Reflection
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
KyuHwang
[DirectX 11] Hierarchical Z-Buffer Occlusion Culling
상단으로

티스토리툴바