1. 程式人生 > >Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第十七章:拾取

Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第十七章:拾取

向上 學習目標 limit rgba mpi hidden param ever 遍歷

原文:Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第十七章:拾取

代碼工程地址:

https://github.com/jiabaodan/Direct12BookReadingNotes



學習目標

學習如何實現拾取算法,我們將它分解為下面幾個步驟:

  1. 當點擊屏幕上s點時,計算對應的透視窗口上的點p;
  2. 在視景坐標系下計算拾取射線;
  3. 將射線和要進行檢測的模型變換到同一個坐標系下;
  4. 檢測模型是否和射線相交,取深度值最小的那個。
    技術分享圖片


1 屏幕透視窗口的變換

第一個需要變換的是,從點擊的屏幕變換到NDC,回顧之前從視景坐標系變換到NDC的變換矩陣:
技術分享圖片
它是通過D3D12_VIEWPORT結構中的數據組成:

typedef struct D3D12_VIEWPORT
{
	FLOAT TopLeftX;
	FLOAT TopLeftY;
	FLOAT Width;
	FLOAT Height;
	FLOAT MinDepth;
	FLOAT MaxDepth;
} D3D12_VIEWPORT;

一般情況下視景是整個後置緩沖,深度緩沖範圍是0~1,所以TopLeftX = 0, TopLeftY = 0, MinDepth = 0, MaxDepth = 1, Width = w, Height = h,那麽變換矩陣可以簡化為:
技術分享圖片


現在令pndc = (xndc, yndc, zndc, 1)是NDC的一個點(?1 ≤ xndc ≤ 1, ?1 ≤ yndc ≤ 1, and 0 ≤ zndc ≤ 1),變換pndc到屏幕坐標系:
技術分享圖片
我們不修改Z值,因為拾取計算不關系深度值在哪個坐標系,那麽2D屏幕上的點ps = (xs, ys)就對應於NDC下的pndc:
技術分享圖片
上面的方式通過NDC點找到了屏幕坐標系下的點ps,但是拾取算法中我們需要通過屏幕上的點找打NDC下的點:
技術分享圖片
現在我們擁有了NDC下面的點,但是為了發射射線,我們需要得到視景坐標系下面的點,回顧第五章6.3.3,我們映射點從視景坐標系到NDC是通過x坐標除以寬高比r:
技術分享圖片

所以直接在X坐標上乘以寬高比即可:
技術分享圖片
再回顧第五章6.3.1,透視窗口是距離原點d=(α2)d = (\frac{\alpha}{2} ),其中a是豎直方向上的角度。那麽我們就可以通過點(xv, yv, d )發射射線,只要計算出d:
技術分享圖片
通過相似三角形:
技術分享圖片
技術分享圖片
那麽我們可以通過點(x′v, y′v, 1)發射射線,和(xv, yv, d )發射的射線是一樣的,在視景坐標系下計算發射射線的代碼如下:

void PickingApp::Pick(int sx, int sy)
{
	XMFLOAT4X4 P = mCamera.GetProj4x4f();
	
	// Compute picking ray in view space.
	float vx = (+2.0f*sx / mClientWidth - 1.0f) / P(0, 0);
	float vy = (-2.0f*sy / mClientHeight + 1.0f) / P(1, 1);
	
	// Ray definition in view space.
	XMVECTOR rayOrigin = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
	XMVECTOR rayDir = XMVectorSet(vx, vy, 1.0f, 0.0f);


2 世界/局部坐標系拾取射線

如果rv(t) = q + tu是世界坐標系下的拾取射線,V是世界坐標系到視景坐標系的變換矩陣,那麽世界坐標系下的拾取射線為:
技術分享圖片
世界坐標系拾取射線對於在世界坐標系下定義的物體比較有用,但是大部分情況下,物體是在它自己的局部坐標系下定義的。如果W是局部坐標系到世界坐標系的變換矩陣,那麽局部坐標系下的射線為:
技術分享圖片
如果在世界坐標系下做檢測,就需要將物體都變換到世界坐標系下;通常情況下物體擁有很多頂點,都變換過去的計算量非常大,所以只把射線變換到每個物體的局部坐標系下做檢測就比較高效。
下面的代碼展示了將一個射線變換到一個物體的局部坐標系:

// Assume nothing is picked to start, so the picked render-item is invisible.
mPickedRitem->Visible = false;

// Check if we picked an opaque render item. A real app might keep a separate
// "picking list" of objects that can be selected.
for(auto ri : mRitemLayer[(int)RenderLayer::Opaque])
{
	auto geo = ri->Geo;
	
	// Skip invisible render-items.
	if(ri->Visible == false)
		continue;
		
	XMMATRIX V = mCamera.GetView();
	XMMATRIX invView = XMMatrixInverse(&XMMatrixDeterminant(V), V);
	XMMATRIX W = XMLoadFloat4x4(&ri->World);
	XMMATRIX invWorld = XMMatrixInverse(&XMMatrixDeterminant(W), W);
	
	// Tranform ray to vi space of Mesh.
	XMMATRIX toLocal = XMMatrixMultiply(invView, invWorld);
	rayOrigin = XMVector3TransformCoord(rayOrigin, toLocal);
	rayDir = XMVector3TransformNormal(rayDir, toLocal);
	
	// Make the ray direction unit length for the intersection tests.
	rayDir = XMVector3Normalize(rayDir);

XMVector3TransformNormal和XMVector3TransformCoord函數都是傳入3D向量,但是XMVector3TransformNormal裏w = 0,XMVector3TransformCoord裏w=1,所以XMVector3TransformNormal用來變換向量,XMVector3TransformCoord用來變換點。



3 射線/網格的相交檢測

下面的代碼展示了射線和三角形的相交檢測,如果有多個三角形相交,取最近的那個三角形:

// If we hit the bounding box of the Mesh, then we might have
// picked a Mesh triangle, so do the ray/triangle tests.
//
// If we did not hit the bounding box, then it is impossible that we hit
// the Mesh, so do not waste effort doing ray/triangle tests.
float tmin = 0.0f;
if(ri->Bounds.Intersects(rayOrigin, rayDir, tmin))
{
	// NOTE: For the demo, we know what to cast the vertex/index data to.
	// If we were mixing formats, some metadata would be needed to figure
	// out what to cast it to.
	auto vertices = (Vertex*)geo->VertexBufferCPU->GetBufferPointer();
	auto indices = (std::uint32_t*)geo->IndexBufferCPU->GetBufferPointer();
	UINT triCount = ri->IndexCount / 3;
	
	// Find the nearest ray/triangle intersection.
	tmin = MathHelper::Infinity;
	for(UINT i = 0; i < triCount; ++i)
	{
		// Indices for this triangle.
		UINT i0 = indices[i * 3 + 0];
		UINT i1 = indices[i * 3 + 1];
		UINT i2 = indices[i * 3 + 2];
		
		// Vertices for this triangle.
		XMVECTOR v0 = XMLoadFloat3(&vertices[i0].Pos);
		XMVECTOR v1 = XMLoadFloat3(&vertices[i1].Pos);
		XMVECTOR v2 = XMLoadFloat3(&vertices[i2].Pos);
		
		// We have to iterate over all the triangles in order to find
		// the nearest intersection.
		float t = 0.0f;
		if(TriangleTests::Intersects(rayOrigin, rayDir, v0, v1, v2, t))
		{
			if(t < tmin)
			{
				// This is the new nearest picked triangle.
				tmin = t;
				UINT pickedTriangle = i;
				
				// Set a render item to the picked triangle so that
				// we can render it with a special "highlight" material.
				mPickedRitem->Visible = true;
				mPickedRitem->IndexCount = 3;
				mPickedRitem->BaseVertexLocation = 0;
				
				// Picked render item needs same world matrix as object picked.
				mPickedRitem->World = ri->World;
				mPickedRitem->NumFramesDirty = gNumFrameResources;
				
				// Offset to the picked triangle in the mesh index buffer.
				mPickedRitem->StartIndexLocation = 3 * pickedTriangle;
			}
		}
	}
}

上面的算法中,我們先進行了物體的包圍體的檢測,這樣可以對性能進行很大的優化;因為只有通過包圍體檢測的物體,才進行逐三角形的相交檢測。

觀察上面的拾取,我們使用系統內存拷貝保存網格幾何數據在MeshGeometry類中。這是因為我們無法讀取vertex/index緩沖。


3.1 射線/AABB的相交檢測

DirectX碰撞檢測庫中BoundingBox::Intersects函數可以用來檢測,返回true就代表已經相交:

bool XM_CALLCONV BoundingBox::Intersects(
	FXMVECTOR Origin, // ray origin
	FXMVECTOR Direction, // ray direction (must be unit length)
	float& Dist ); const // ray intersection parameter

給出射線r(t) = q + tu,最後一個參數就是t0計算出點P:
技術分享圖片


3.2 射線/球體的相交檢測

DirectX碰撞檢測庫中的函數:

bool XM_CALLCONV BoundingSphere::Intersects(
	FXMVECTOR Origin,
	FXMVECTOR Direction,
	float& Dist ); const

3.3 射線/三角形的相交檢測

DirectX碰撞檢測庫中的函數:

bool XM_CALLCONV TriangleTests::Intersects(
	FXMVECTOR Origin, // ray origin
	FXMVECTOR Direction, // ray direction (unit length)
	FXMVECTOR V0, // triangle vertex v0
	GXMVECTOR V1, // triangle vertex v1
	HXMVECTOR V2, // triangle vertex v2
	float& Dist ); // ray intersection parameter


4 Demo應用

本例子中渲染了一個小車,可以讓用戶使用右鍵拾取網格中的三角形。為了實現拾取的三角形的高亮顯示,本例中的Render-Item和以前的有些不同,只能部分在初始化中填充。我們添加了一個Visible屬性,不可見的Render-Item將不繪制。下面的代碼展示了如何根據拾取設置Render-Item屬性:
技術分享圖片

// Cache a pointer to the render-item of the picked
// triangle in the PickingApp class.
RenderItem* mPickedRitem;

if(TriangleTests::Intersects(rayOrigin, rayDir, v0, v1, v2, t))
{
	if(t < tmin)
	{
		// This is the new nearest picked triangle.
		tmin = t;
		UINT pickedTriangle = i;
		
		// Set a render item to the picked triangle so that
		// we can render it with a special "highlight" material.
		mPickedRitem->Visible = true;
		mPickedRitem->IndexCount = 3;
		mPickedRitem->BaseVertexLocation = 0;
		
		// Picked render item needs same world matrix as object picked.
		mPickedRitem->World = ri->World;
		mPickedRitem->NumFramesDirty = gNumFrameResources;
		
		// Offset to the picked triangle in the mesh index buffer.
		mPickedRitem->StartIndexLocation = 3 * pickedTriangle;
	}
}

這個render-item是在繪制完不透明render-item後繪制的,使用一個高亮度的PSO。需要註意的是深度測試使用的是D3D12_COMPARISON_FUNC_LESS_EQUAL,因為如果使用D3D12_COMPARISON_FUNC_LESS會導致深度測試失敗而不繪制:

DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]);

mCommandList->SetPipelineState(mPSOs["highlight"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Highlight]);


5 總結

  1. 拾取技術是判定用戶在屏幕上點擊的2D投射的物體,與之對應的3D物體;
  2. 拾取射線是從視景坐標系的原點發射出的一條射線,經過透視窗口上與用戶點擊屏幕的點對應的點;
  3. 我們可以用過變換射線的原點和方向向量來變換射線所在的坐標系(頂點w = 1,向量w = 0);
  4. 為了判定射線和物體是否相交,我們對物體的每一個三角形進行射線/三角形判定,如果有多個三角形相交,我們選擇最近的那一個;
  5. 為了優化考慮,我們先進行物體包圍體檢測,只有檢測通過的物體再遍歷每個三角形檢測。


6 練習

調查八叉樹,優化拾取檢測;之前提到的截頭錐體剔除,也可以優化拾取檢測。
(物理和碰撞也同理)

Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第十七章:拾取