在上一個教程中,咱們在應用程序窗口的中心成功渲染了一個三角形。 咱們沒有太注意咱們在頂點緩衝區中拾取的頂點位置。 在本教程中,咱們將深刻研究3D位置和轉換的細節。git
本教程的結果將是渲染到屏幕的3D對象。 雖然以前的教程側重於將2D對象渲染到3D世界,但在這裏咱們展現了一個3D對象。github
(SDK root)\Samples\C++\Direct3D11\Tutorials\Tutorial04數組
Github倉庫框架
在上一個教程中,三角形的頂點被有策略地放置,以在屏幕上完美地對齊。 可是,狀況並不是老是如此。 所以,咱們須要一個系統來表示3D空間中的對象和一個顯示它們的系統。函數
在現實世界中,物體存在於3D空間中。 這意味着要將對象放置在世界中的特定位置,咱們須要使用座標系並定義與位置對應的三個座標。 在計算機圖形學中,3D空間最經常使用於笛卡爾座標系。 在該座標系中,三個軸X,Y和Z彼此垂直,決定了空間中每一個點的座標。 該座標系進一步分爲左手系統和右手系統。 在左手系統中,當X軸指向右側,Y軸指向上方時,Z軸指向前方。 在右手系統中,具備相同的X和Y軸,Z軸指向後方。佈局
圖1.左手座標系與右手座標系spa
如今咱們已經討論過座標系,考慮3D空間。 點在不一樣的空間中具備不一樣的座標。 做爲一維中的一個例子,假設咱們有一個標尺,咱們注意到標尺的5英寸標記處的點P. 如今,若是咱們將標尺向右移動1英寸,則相同的點位於4英寸標記處。 經過移動標尺,參考框架已經改變。 所以,當點沒有移動時,它有一個新的座標。設計
圖2. 1D中的空間圖示3d
在3D中,空間一般由原點和來自原點的三個惟一軸定義:X,Y和Z.計算機圖形中一般使用多個空間:對象空間,世界空間,視圖空間,投影空間和屏幕空間。指針
圖3.在對象空間中定義的立方體
(-1, 1, -1) ( 1, 1, -1) (-1, -1, -1) ( 1, -1, -1) (-1, 1, 1) ( 1, 1, 1) (-1, -1, 1) ( 1, -1, 1)
左圖顯示了一個場景,該場景由相似人的物體和觀察物體的觀察者(相機)組成。 世界空間使用的原點和軸以紅色顯示。 右圖顯示了與世界空間相關的視圖空間。 視圖空間軸顯示爲藍色。 爲了更清楚地說明,視圖空間與左圖像中的世界空間的方向與讀者不一樣。 請注意,在視圖空間中,查看器正在Z方向上查看。
投影空間是指從視圖空間應用投影變換後的空間。 在此空間中,可見內容的X和Y座標範圍爲-1到1,Z座標範圍爲0到1。
屏幕空間一般用於指代幀緩衝區中的位置。 由於幀緩衝區一般是2D紋理,因此屏幕空間是2D空間。 左上角是座標爲(0,0)的原點。 正X向右,正Y向下。 對於w像素寬且h像素高的緩衝區,最右下像素具備座標(w-1,h-1)。
轉換最經常使用於將頂點從一個空間轉換爲另外一個空間。 在3D計算機圖形學中,管道中邏輯上有三種這樣的變換:世界,視圖和投影變換。 下一個教程將介紹單個轉換操做,如轉換,旋轉和縮放。
顧名思義,世界轉換將頂點從對象空間轉換爲世界空間。 它一般由一個或多個縮放,旋轉和平移組成,基於咱們想要給對象的大小,方向和位置。 場景中的每一個對象都有本身的世界變換矩陣。 這是由於每一個對象都有本身的大小,方向和位置。
頂點轉換爲世界空間後,視圖轉換將這些頂點從世界空間轉換爲視圖空間。 回想一下以前的討論,觀看空間是世界從觀衆(或相機)的角度出現的。 在視圖空間中,觀察者位於沿正Z軸向外看的原點。
值得注意的是,儘管視圖空間是來自觀察者參照系的世界,但視圖變換矩陣應用於頂點,而不是觀察者。 所以,視圖矩陣必須執行咱們應用於咱們的查看器或相機的相反轉換。 例如,若是咱們想要將攝像機朝向-Z方向移動5個單元,咱們須要計算一個視圖矩陣,它能夠沿着+ Z方向將頂點平移5個單位。 雖然相機向後移動,但從相機的角度來看,頂點已向前移動。 在XNA Math中,一個方便的API調用XMMatrixLookAtLH()一般用於計算視圖矩陣。 咱們只須要告訴它觀察者在哪裏,在哪裏看,以及表示觀察者頂部的方向,也稱爲向上矢量,以得到相應的視圖矩陣。
投影變換將頂點從諸如世界和視圖空間的3D空間轉換爲投影空間。 在投影空間中,頂點的X和Y座標是從3D空間中該頂點的X / Z和Y / Z比得到的。
圖5.投影
在3D空間中,事物以透視的方式出現。 也就是說,物體越近,它出現的越大。 如圖所示,在遠離觀察者眼睛的d個單位處高h單位的樹的尖端將出如今與另外一棵樹的尖端2h單位高和2d單位遠的相同點處。 所以,在2D屏幕上出現頂點的位置與其X / Z和Y / Z比率直接相關。
定義3D空間的參數之一稱爲視場(FOV)。 FOV表示在特定方向上查看哪些對象從特定位置可見。 人類有一個前瞻性的FOV(咱們沒法看到咱們背後的東西),咱們看不到太近或太遠的物體。 在計算機圖形學中,FOV包含在視錐體中。 視錐體由3D中的6個平面定義。 這些平面中的兩個平行於XY平面。 這些被稱爲近Z和遠Z平面。 其餘四個平面由觀察者的水平和垂直視野定義。 視場越寬,視錐體體積越寬,觀察者看到的物體越多。
GPU會過濾掉視錐體外的對象,這樣就沒必要花時間渲染沒法顯示的內容。 此過程稱爲裁剪。 視錐體是一個四面金字塔,頂部被切掉。 剪切此卷是很複雜的,由於要剪切一個視錐體平面,GPU必須將每一個頂點與平面的等式進行比較。 相反,GPU一般首先執行投影變換,而後針對視錐體量進行剪輯。 投影變換對視錐體的影響是金字塔形視錐體成爲投影空間中的盒子。 這是由於,如前所述,在投影空間中,X和Y座標基於3D空間中的X / Z和Y / Z. 所以,點a和點b在投影空間中將具備相同的X和Y座標,這就是視錐體成爲盒子的緣由。
圖6.查看平截頭體
假設兩棵樹的尖端剛好位於頂視圖平截頭體邊緣。進一步假設d = 2h。沿投影空間中頂邊的Y座標將爲0.5(由於h / d = 0.5)。所以,任何大於0.5的Y投影后Y值都將被裁剪。這裏的問題是0.5由程序選擇的垂直視場肯定,而且不一樣的FOV值致使GPU必須剪切的不一樣值。爲了使這個過程更加方便,3D程序一般縮放頂點的投影X和Y值,以即可見的X和Y值的範圍從-1到1.換句話說,任何X或Y座標都在[-1]以外1]範圍將被刪除。爲了使該剪切方案起做用,投影矩陣必須經過h / d或d / h的倒數來縮放投影頂點的X和Y座標。 d / h也是FOV一半的餘切。經過縮放,視錐體的頂部變爲h / d * d / h = 1.大於1的任何內容都將被GPU裁剪。這就是咱們想要的。
一般也對投影空間中的Z座標進行相似的調整。 咱們但願近和遠Z平面分別在投影空間中爲0和1。 當Z = 3D空間中的近Z值時,Z在投影空間中應爲0; 當Z = 3D空間中的遠Z時,Z在投影空間中應爲1。 完成此操做後,GPU [0 1]之外的任何Z值都將被裁剪掉。
在Direct3D 11中,獲取投影矩陣的最簡單方法是調用XMMatrixPerspectiveFovLH()方法。 咱們只提供4個參數-FOVy,Aspect,Zn和Zf-並返回一個矩陣,它能夠完成上面提到的全部必要操做。 FOVy是Y方向的視野。 Aspect是寬高比,即視圖寬度與高度的比率。 從FOVy和Aspect,能夠計算FOVx。 該縱橫比一般從渲染目標寬度與高度的比率得到。 Zn和Zf分別是視圖空間中的近和遠Z值。
在上一個教程中,咱們編寫了一個程序,用於渲染單個三角形。 當咱們建立頂點緩衝區時,咱們使用的頂點位置直接在投影空間中,這樣咱們就沒必要執行任何變換。 如今咱們已經瞭解了3D空間和變換,咱們將修改程序,以便在對象空間中定義頂點緩衝區,就像它應該的那樣。 而後,咱們將修改頂點着色器以將頂點從對象空間轉換爲投影空間。
因爲咱們開始以三維方式表示事物,所以咱們將前一個教程中的平面三角形更改成多維數據集。 這將使咱們可以更清楚地展現這些概念。
SimpleVertex vertices[] = { { XMFLOAT3( -1.0f, 1.0f, -1.0f ), XMFLOAT4( 0.0f, 0.0f, 1.0f, 1.0f ) }, { XMFLOAT3( 1.0f, 1.0f, -1.0f ), XMFLOAT4( 0.0f, 1.0f, 0.0f, 1.0f ) }, { XMFLOAT3( 1.0f, 1.0f, 1.0f ), XMFLOAT4( 0.0f, 1.0f, 1.0f, 1.0f ) }, { XMFLOAT3( -1.0f, 1.0f, 1.0f ), XMFLOAT4( 1.0f, 0.0f, 0.0f, 1.0f ) }, { XMFLOAT3( -1.0f, -1.0f, -1.0f ), XMFLOAT4( 1.0f, 0.0f, 1.0f, 1.0f ) }, { XMFLOAT3( 1.0f, -1.0f, -1.0f ), XMFLOAT4( 1.0f, 1.0f, 0.0f, 1.0f ) }, { XMFLOAT3( 1.0f, -1.0f, 1.0f ), XMFLOAT4( 1.0f, 1.0f, 1.0f, 1.0f ) }, { XMFLOAT3( -1.0f, -1.0f, 1.0f ), XMFLOAT4( 0.0f, 0.0f, 0.0f, 1.0f ) }, };
若是你注意到咱們所作的只是指定立方體上的八個點,但咱們實際上沒有描述各個三角形。 若是咱們按原樣傳遞,輸出將不是咱們所指望的。 咱們須要經過這八個點指定造成立方體的三角形。
在立方體上,許多三角形將共享相同的頂點,而且一次又一次地從新定義相同的點將浪費空間。 所以,有一種方法只指定八個點,而後讓Direct3D知道要爲三角形選擇哪些點。 這是經過索引緩衝區完成的。 索引緩衝區將包含一個列表,該列表將引用緩衝區中的頂點索引,以指定在每一個三角形中使用哪些點。 下面的代碼顯示了構成每一個三角形的點。
// Create index buffer WORD indices[] = { 3,1,0, 2,1,3, 0,5,4, 1,5,0, 3,4,7, 0,4,3, 1,6,5, 2,6,1, 2,7,6, 3,7,2, 6,4,5, 7,4,6, };
如您所見,第一個三角形由點3,1和0定義。這意味着第一個三角形的頂點位於:( - 1.0f,1.0f,1.0f),(1.0f,1.0f,-1.0) f),和(-1.0f,1.0f,-1.0f)。 立方體上有六個面,每一個面由兩個三角形組成。 所以,您會看到此處定義的12個三角形。
因爲每一個頂點都是明確列出的,而且沒有兩個三角形共享邊(至少,它已經被定義),這被認爲是一個三角形列表。 總的來講,對於三角形列表中的12個三角形,咱們將須要總共36個頂點。
索引緩衝區的建立與頂點緩衝區很是類似,咱們在結構中指定了諸如大小和類型之類的參數,並稱爲CreateBuffer。 類型是D3D11_BIND_INDEX_BUFFER,由於咱們使用DWORD聲明瞭咱們的數組,因此咱們將使用sizeof(DWORD)。
D3D11_BUFFER_DESC bd; ZeroMemory( &bd, sizeof(bd) ); bd.Usage = D3D11_USAGE_DEFAULT; bd.ByteWidth = sizeof( WORD ) * 36; // 36 vertices needed for 12 triangles in a triangle list bd.BindFlags = D3D11_BIND_INDEX_BUFFER; bd.CPUAccessFlags = 0; bd.MiscFlags = 0; InitData.pSysMem = indices; if( FAILED( g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pIndexBuffer ) ) ) return FALSE;
一旦咱們建立了這個緩衝區,咱們就須要設置它,以便Direct3D知道在生成三角形時引用這個索引緩衝區。 咱們指定緩衝區的指針,格式和緩衝區中的偏移量以開始引用。
// Set index buffer g_pImmediateContext->IASetIndexBuffer( g_pIndexBuffer, DXGI_FORMAT_R16_UINT, 0 );
在上一個教程的頂點着色器中,咱們採用輸入頂點位置並輸出相同的位置而不進行任何修改。咱們能夠這樣作,由於輸入頂點位置已經在投影空間中定義。如今,由於輸入頂點位置是在對象空間中定義的,因此咱們必須在從頂點着色器輸出以前對其進行變換。咱們經過三個步驟完成此任務:從對象轉換到世界空間,從世界轉換到視圖空間,以及從視圖轉換到投影空間。咱們須要作的第一件事是聲明三個常量緩衝區變量。常量緩衝區用於存儲應用程序須要傳遞給着色器的數據。在渲染以前,應用程序一般會將重要數據寫入常量緩衝區,而後在渲染過程當中能夠從着色器中讀取數據。在FX文件中,常量緩衝區變量在C ++結構中聲明爲全局變量。咱們將使用的三個變量是HLSL類型「矩陣」的世界,視圖和投影變換矩陣。
一旦咱們聲明瞭咱們須要的矩陣,咱們就會更新頂點着色器以使用矩陣變換輸入位置。 經過將矢量乘以矩陣來變換矢量。 在HLSL中,這是使用mul()內部函數完成的。 咱們的變量聲明和新的頂點着色器以下所示:
cbuffer ConstantBuffer : register( b0 ) { matrix World; matrix View; matrix Projection; } // // Vertex Shader // VS_OUTPUT VS( float4 Pos : POSITION, float4 Color : COLOR ) { VS_OUTPUT output = (VS_OUTPUT)0; output.Pos = mul( Pos, World ); output.Pos = mul( output.Pos, View ); output.Pos = mul( output.Pos, Projection ); output.Color = Color; return output; }
在頂點着色器中,每一個 mul()將一個變換應用於輸入位置。 世界,視圖和投影變換按順序依次應用。 這是必要的,由於向量和矩陣乘法不是可交換的。
咱們更新了頂點着色器以使用矩陣進行變換,但咱們還須要在程序中定義三個矩陣。 這三個矩陣將存儲渲染時要使用的變換。 在渲染以前,咱們將這些矩陣的值複製到着色器常量緩衝區。 而後,當咱們經過調用Draw()啓動渲染時,咱們的頂點着色器讀取存儲在常量緩衝區中的矩陣。 除了矩陣以外,咱們還須要一個表明常量緩衝區的ID3D11Buffer對象。 所以,咱們的全局變量將添加如下內容:
ID3D11Buffer* g_pConstantBuffer = NULL; XMMATRIX g_World; XMMATRIX g_View; XMMATRIX g_Projection;
要建立ID3D11Buffer對象,咱們使用 ID3D11Device :: CreateBuffer()並指定D3D11_BIND_CONSTANT_BUFFER
D3D11_BUFFER_DESC bd; ZeroMemory( &bd, sizeof(bd) ); bd.Usage = D3D11_USAGE_DEFAULT; bd.ByteWidth = sizeof(ConstantBuffer); bd.BindFlags = D3D11_BIND_CONSTANT_BUFFER; bd.CPUAccessFlags = 0; if( FAILED(g_pd3dDevice->CreateBuffer( &bd, NULL, &g_pConstantBuffer ) ) ) return hr;
咱們須要作的下一件事是提出三個矩陣,咱們將用它來進行轉換。咱們但願三角形位於原點上,與XY平面平行。這正是它如何存儲在對象空間中的頂點緩衝區中。所以,世界變換不須要作任何事情,咱們將世界矩陣初始化爲單位矩陣。咱們想要設置咱們的相機,使其位於[0 1 -5],查看點[0 1 0]。咱們可使用向上矢量[0 1 0]調用 XMMatrixLookAtLH()來方便地爲咱們計算視圖矩陣,由於咱們但願+ Y方向始終保持在頂部。最後,爲了獲得投影矩陣,咱們稱之爲XMMatrixPerspectiveFovLH(),具備90度垂直視場(pi / 2),寬高比爲640/480,來自咱們的後緩衝區大小,以及近和遠Z分別爲0.1和110。這意味着屏幕上將看不到小於0.1或超過110的任何內容。這三個矩陣存儲在全局變量g_World,g_View和g_Projection中。
咱們有矩陣,如今咱們必須在渲染時將它們寫入常量緩衝區,以便GPU能夠讀取它們。 要更新緩衝區,咱們可使用 ID3D11DeviceContext :: UpdateSubresource()API並將指針傳遞給以與着色器常量緩衝區相同的順序存儲的矩陣。 爲了作到這一點,咱們將建立一個與着色器中的常量緩衝區具備相同佈局的結構。 另外,因爲矩陣在C ++和HLSL中的內存排列方式不一樣,咱們必須在更新以前轉置矩陣。
// // Update variables // ConstantBuffer cb; cb.mWorld = XMMatrixTranspose( g_World ); cb.mView = XMMatrixTranspose( g_View ); cb.mProjection = XMMatrixTranspose( g_Projection ); g_pImmediateContext->UpdateSubresource( g_pConstantBuffer, 0, NULL, &cb, 0, 0 );