19-地形渲染

地形渲染的想法是從一個平坦的網格開始(圖19.1的頂部)。 然後我們調整頂點的高度(即y座標),使網格模型平滑地從山到谷的過渡,從而模擬一個地形(圖19.1的中間)。 當然,我們應用了一個很好的紋理來渲染沙灘,草地,岩石峭壁和雪山(圖19.1的底部)。
目標:
1.瞭解如何爲地形生成高度信息,以便在山谷之間實現平滑過渡。
2.瞭解如何紋理地形。
3.使用硬件細分來渲染具有連續細節層次的地形。
4.發現一種方法來保持相機或其他物體種植在地形表面上。


圖19.1 (頂部)三角形網格。(中)平滑高度過渡的三角形網格用於創建山丘和山谷。 (底部)加了光照和紋理的地形。

19.1 高度圖

我們使用高度圖來描述我們地形的山丘和山谷。高度圖是矩陣,其中每個元素指定地形網格中特定頂點的高度。也就是說,在每個網格頂點的高度圖中存在一個條目,並且第ij個高度圖條目提供了第i個頂點的高度。 通常情況下,高度圖在圖像編輯器中用圖形表示爲灰度圖,其中黑色表示最小高度,白色表示最大高度,灰色陰影表示高度之間。 圖19.2顯示了一些高度圖和它們構造的相應地形的例子。


圖19.2 高度圖的例子。觀察高度圖所描述的高度如何構建不同的地形曲面。

當我們將高度貼圖存儲在磁盤上時,我們通常爲高度圖中的每個元素分配一個內存字節,因此高度範圍可以從0到255之間。範圍0到255足以保持地形高度之間的過渡,但 在我們的應用程序中,我們可能需要擴展0到255範圍以匹配3D世界的規模。 例如,如果我們在3D世界中的度量單位是英尺,那麼0到255不會給我們足夠的值來表示任何有趣的值。 出於這個原因,當我們將數據加載到我們的應用程序中時,我們爲每個高度元素分配一個浮點數。 這使我們可以在0到255的範圍之外進行擴展,以匹配任何所需的比例; 此外,它還使我們能夠過濾高度圖並生成整數之間的高度值。

NOTE:在§6.11中,我們使用數學函數創建了一個“地形”。 這是程序生成的地形的一個例子。 然而,很難想出一個精確描述你想要的地形的函數。 高度圖給予更多的靈活性,因爲它們可以由繪畫程序或高度圖編輯工具中的藝術家編輯。

19.1.1創建一個高度圖

可以通過程序或圖像編輯器(如Adobe Photoshop)生成高度圖。 使用油漆過濾器生成不同的混沌高度圖模式可以證明是一個好的開始; 那麼可以通過利用繪圖編輯器的工具手動調整高度圖。 應用模糊濾鏡可用於平滑高度貼圖中的粗糙邊緣。

Terragen程序(http://www.planetside.co.uk/)可以通過程序生成高度貼圖,它還提供修改高度貼圖的工具(或者可以導出高度貼圖,然後在一個單獨的繪圖程序中導入和修改,如Photoshop中)。 Bryce程序(http://www.daz3d.com/i.x/software/bryce/-/)還有許多用於生成高度圖的程序算法,以及內置的高度圖編輯器。 Dark Tree(http://www.darksim.com/)是一個功能強大的程序紋理創作程序,尤其可用於創建灰度高度圖。

完成繪製高度圖後,需要將其保存爲8位RAW文件。 RAW文件一個接一個地包含圖像的字節。 這使得將圖像讀入我們的程序非常容易。 您的軟件可能會要求您使用標題保存RAW文件; 不指定標題。 圖19.3顯示了Terragen的導出對話框。


圖19.3 (左)景觀生成器允許您以程序方式生成隨機地形,並使用畫筆工具手動雕刻地形。 (右)Terragen的導出對話框。 注意所選的導出方法是8位RAW格式。

NOTE:您不必使用RAW格式來存儲您的高度圖; 你可以使用任何適合你需要的格式。 RAW格式只是我們可以使用的格式的一個例子。 我們決定使用RAW格式,因爲許多圖像編輯器可以導出爲這種格式,並且可以非常容易地將RAW文件中的數據加載到我們的程序演示中。 本書中的演示使用8位RAW文件(即,高度圖中的每個元素都是8位整數)。

NOTE:如果256個高度步長對於您的需要太粗糙,您可以考慮存儲16位高度圖,其中每個高度條目由16位整數表示。 Terragen可以導出16位RAW高度圖。

19.1.2 加載RAW文件

由於RAW文件不過是一個連續的字節塊(其中每個字節都是一個高度圖條目),所以我們可以輕鬆地在一個std :: ifstream :: read調用中讀取內存塊,就像在下一個方法中所做的那樣:

void Terrain::LoadHeightmap()
{
// A height for each vertex
std::vector<unsigned char> in(
mInfo.HeightmapWidth * mInfo.HeightmapHeight);
// Open the file.
std::ifstream inFile;
inFile.open(mInfo.HeightMapFilename.c_str(),
std::ios_base::binary);
if(inFile)
{
// Read the RAW bytes.
inFile.read((char*)&in[0], (std::streamsize)in.size());
// Done with file.
inFile.close();
}
// Copy the array data into a float array and scale it.
mHeightmap.resize(mInfo.HeightmapHeight * mInfo.HeightmapWidth, 0);
for(UINT i = 0; i < mInfo.HeightmapHeight * mInfo.HeightmapWidth; ++i)
{
mHeightmap[i] = (in[i] / 255.0f)*mInfo.HeightScale;
}
}

mInfo變量是Terrain類的成員,它是以下結構的一個實例,它描述了地形的各種屬性:

struct InitInfo
{
// Filename of RAW heightmap data.
std::wstring HeightMapFilename;
// Texture filenames used for texturing the terrain.
std::wstring LayerMapFilename0;
std::wstring LayerMapFilename1;
std::wstring LayerMapFilename2;
std::wstring LayerMapFilename3;
std::wstring LayerMapFilename4;
std::wstring BlendMapFilename;
// Scale to apply to heights after they have been
// loaded from the heightmap.
float HeightScale;
// Dimensions of the heightmap.
UINT HeightmapWidth;
UINT HeightmapHeight;
// The cell spacing along the x- and z-axes (see Figure (19.4)).
float CellSpacing;
};

NOTE:讀者可能希望查看§5.11的網格結構。


圖19.4 網格屬性。


圖19.5 (a)在[0,255]範圍內的浮點高度值。(b)高度值被鉗位到最近的整數。

19.1.3 平滑

使用8位高度圖的問題之一是,這意味着我們只能表示256個高度的謹慎步驟。 因此,我們不能模擬圖19.5a中顯示的高度值; 相反,我們結束了圖19.5b。 這種截斷創建了一個“粗糙”的地形,而這並非意圖。 當然,一旦我們截斷,我們不能恢復原始高度值,但通過平滑圖19.5b,我們可以得到接近19.5a的東西。

所以我們通過讀取原始字節將高度圖加載到內存中。 然後,我們將字節數組複製到浮點數組中,以便我們具有浮點精度。 然後,我們將過濾器應用於浮點高度圖,該高度圖平滑了高度圖,使相鄰元素之間的高度差別不那麼劇烈。 我們使用的過濾算法非常基礎。 通過對自身及其8個相鄰像素進行平均來計算新的濾波後的高度圖像素(圖19.6):

hi,j~=hi1,j1+hi1,j+hi1,j+1+hi,j1+hi,j+hi,j+1+hi+1,j1+hi+1,j+hi+1,j+19


圖19.6 第i個頂點的高度可以通過平均第i個高度圖條目及其8個鄰居高度來找到。

在我們位於高度圖邊緣的情況下,一個像素沒有八個相鄰像素,那麼我們只需要儘可能多的相鄰像素。

以下是平均高度圖中第ij個像素的函數的實現:

bool Terrain::InBounds(int i, int j)
{
// True if ij are valid indices; false otherwise.
return
i >= 0 && i < (int)mInfo.HeightmapHeight &&
j >= 0 && j < (int)mInfo.HeightmapWidth;
}
float Terrain::Average(int i, int j)
{
// Function computes the average height of the ij element.
// It averages itself with its eight neighbor pixels. Note
// that if a pixel is missing neighbor, we just don't include it
// in the average--that is, edge pixels don't have a neighbor pixel.
//
// ----------
// | 1| 2| 3|
// ----------
// |4 |ij| 6|
// ----------
// | 7| 8| 9|
// ----------
float avg = 0.0f;
float num = 0.0f;
// Use int to allow negatives. If we use UINT, @ i=0, m=i-1=UINT_MAX
// and no iterations of the outer for loop occur.
for(int m = i-1; m <= i+1; ++m)
{
for(int n = j-1; n <= j+1; ++n)
{
if(InBounds(m,n))
{
avg += mHeightmap[m*mInfo.HeightmapWidth + n];
num += 1.0f;
}
}
}
return avg / num;
}

如果條目位於高度圖上,則函數inBounds返回true,否則返回false。 因此,如果我們嘗試對邊緣上不屬於高度圖的條目旁邊的元素進行採樣,那麼inBounds將返回false,並且我們不會將其包含在我們的平均值中 - 它不存在。

爲了平滑整個高度圖,我們只對每個高度圖條目應用平均值:

void Terrain::Smooth()
{
std::vector<float> dest(mHeightmap.size());
for(UINT i = 0; i < mInfo.HeightmapHeight; ++i)
{
for(UINT j = 0; j < mInfo.HeightmapWidth; ++j)
{
dest[i*mInfo.HeightmapWidth+j] = Average(i,j);
}
}/
/ Replace the old heightmap with the filtered one.
mHeightmap = dest;
}

19.1.4高度貼圖着色器資源視圖

正如我們將在下一節中看到的,爲了支持曲面細分和位移貼圖,我們需要在我們的着色器程序中對高度貼圖進行採樣。 因此,我們必須爲高度圖創建着色器資源視圖。 這應該是現在熟悉的練習; 唯一的技巧是爲了節省內存,我們使用16位浮點數而不是32位浮點數。 要將32位浮點數轉換爲16位浮點數,我們使用XNA數學函數XMConvertFloatToHalf。

void Terrain::BuildHeightmapSRV(ID3D11Device* device)
{
D3D11_TEXTURE2D_DESC texDesc;
texDesc.Width = mInfo.HeightmapWidth;
texDesc.Height = mInfo.HeightmapHeight;
texDesc.MipLevels = 1;
texDesc.ArraySize = 1;
texDesc.Format = DXGI_FORMAT_R16_FLOAT;
texDesc.SampleDesc.Count = 1;
texDesc.SampleDesc.Quality = 0;
texDesc.Usage = D3D11_USAGE_DEFAULT;
texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
texDesc.CPUAccessFlags = 0;
texDesc.MiscFlags = 0;
// HALF is defined in xnamath.h, for storing 16-bit float.
std::vector<HALF> hmap(mHeightmap.size());
std::transform(mHeightmap.begin(), mHeightmap.end(),
hmap.begin(), XMConvertFloatToHalf);
D3D11_SUBRESOURCE_DATA data;
data.pSysMem = &hmap[0];
data.SysMemPitch = mInfo.HeightmapWidth*sizeof(HALF);
data.SysMemSlicePitch = 0;
ID3D11Texture2D* hmapTex = 0;
HR(device->CreateTexture2D(&texDesc, &data, &hmapTex));
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
srvDesc.Format = texDesc.Format;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MostDetailedMip = 0;
srvDesc.Texture2D.MipLevels = -1;
HR(device->CreateShaderResourceView(
hmapTex, &srvDesc, &mHeightMapSRV));
// SRV saves reference.
ReleaseCOM(hmapTex);
}

19.2地形斜紋

地形覆蓋大面積,因此建造它們所需的三角形數量很大。 通常,地形需要一個詳細程度(LOD)系統。 也就是說,遠離相機的地形部分不需要多少三角形,因爲細節不被察覺; 見圖19.7。


圖19.7 詳細程度隨着距相機的距離而減少。 我們的地形曲面細分策略如下:

1.放下四個補丁網格。
2.根據它們距相機的距離對貼片進行細分。
3.將高度圖綁定爲着色器資源。 在域着色器中,從高度圖執行位移映射以將生成的頂點偏移到其正確的高度。

19.2.1網格構造

假設我們的高度圖具有尺寸(2m+1)×(2n+1) 。我們可以從這個高度圖創建的最詳細的地形是(2m+1)×(2n+1) 個頂點的網格; 因此,這代表了我們最大的曲面細分的地形網格,我們將其稱爲T0T0 的頂點之間的單元格間距由InitInfo :: CellSpacing屬性(第9.1.2節)給出。也就是說,當我們引用單元格時,我們正在討論最細化的網格T0 的單元格。

我們將地形劃分成一個補丁網格,使得每個補丁覆蓋T0的65×65個頂點的塊(見圖19.8)。 我們選擇65是因爲最大麴面細分因子是64,因此我們可以將一個補丁拼湊成64×64個單元,這些單元與65×65個頂點相交。 因此,如果某個修補程序得到最大程度的細化,則它會從每個生成的頂點所需的高度圖中獲取信息。 如果一個補丁具有曲面細分系數1,則該補丁不會被細分,而只會呈現爲兩個三角形。 因此,補丁網格可以被認爲是地形最粗糙的棋盤格版本。


圖19.8 爲了說明的目的,我們使用較小的數字。 最大棋盤格地形網格具有17×25個頂點和16×24個單元格。我們將網格劃分爲一個補丁網格,每個補丁網格覆蓋8×8個網格或9×9個頂點。這導致了2×3的補丁網格。

貼片頂點網格尺寸的計算公式如下:

static const int CellsPerPatch = 64;
// Divide heightmap into patches such that each patch has CellsPerPatch.
mNumPatchVertRows = ((mInfo.HeightmapHeight-1) / CellsPerPatch) + 1;
mNumPatchVertCols = ((mInfo.HeightmapWidth-1) / CellsPerPatch) + 1;

並且貼片頂點和四元貼片基元的總數由以下公式計算:

mNumPatchVertices = mNumPatchVertRows*mNumPatchVertCols;
mNumPatchQuadFaces = (mNumPatchVertRows-1)*(mNumPatchVertCols-1);

我們的地形補丁頂點結構如下所示:

struct Terrain
{
XMFLOAT3 Pos;
XMFLOAT2 Tex;
XMFLOAT2 BoundsY;
};

BoundsY屬性將在§19.3中解釋。生成四色塊頂點和索引緩衝區的代碼如下所示:

float Terrain::GetWidth()const
{
// Total terrain width.
return (mInfo.HeightmapWidth-1)*mInfo.CellSpacing;
}
float Terrain::GetDepth()const
{
// Total terrain depth.
return (mInfo.HeightmapHeight-1)*mInfo.CellSpacing;
}
void Terrain::BuildQuadPatchVB(ID3D11Device* device)
{
std::vector<Vertex::Terrain> patchVertices(mNumPatchVertRows*mNumPatchVertCols);
float halfWidth = 0.5f*GetWidth();
float halfDepth = 0.5f*GetDepth();
float patchWidth = GetWidth() / (mNumPatchVertCols-1);
float patchDepth = GetDepth() / (mNumPatchVertRows-1);
float du = 1.0f / (mNumPatchVertCols-1);
float dv = 1.0f / (mNumPatchVertRows-1);
for(UINT i = 0; i < mNumPatchVertRows; ++i)
{
float z = halfDepth - i*patchDepth;
for(UINT j = 0; j < mNumPatchVertCols; ++j)
{
float x = -halfWidth + j*patchWidth;
patchVertices[i*mNumPatchVertCols+j].Pos = XMFLOAT3(x, 0.0f, z);
// Stretch texture over grid.
patchVertices[i*mNumPatchVertCols+j].Tex.x = j*du;
patchVertices[i*mNumPatchVertCols+j].Tex.y = i*dv;
}
}
// Store axis-aligned bounding box y-bounds in upper-left patch corner.
for(UINT i = 0; i < mNumPatchVertRows-1; ++i)
{
for(UINT j = 0; j < mNumPatchVertCols-1; ++j)
{
UINT patchID = i*(mNumPatchVertCols-1)+j;
patchVertices[i*mNumPatchVertCols+j].BoundsY = mPatchBoundsY[patchID];
}
}
D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof(Vertex::Terrain) * patchVertices.size();
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
vbd.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA vinitData;
vinitData.pSysMem = &patchVertices[0];
HR(device->CreateBuffer(&vbd, &vinitData, &mQuadPatchVB));
}
void Terrain::BuildQuadPatchIB(ID3D11Device* device)
{
std::vector<USHORT> indices(mNumPatchQuadFaces*4); // 4 indices per quad face
// Iterate over each quad and compute indices.
int k = 0;
for(UINT i = 0; i < mNumPatchVertRows-1; ++i)
{
for(UINT j = 0; j < mNumPatchVertCols-1; ++j)
{
// Top row of 2x2 quad patch
indices[k] = i*mNumPatchVertCols+j;
indices[k+1] = i*mNumPatchVertCols+j+1;
// Bottom row of 2x2 quad patch
indices[k+2] = (i+1)*mNumPatchVertCols+j;
indices[k+3] = (i+1)*mNumPatchVertCols+j+1;
k += 4; // next quad
}
}
D3D11_BUFFER_DESC ibd;
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof(USHORT) * indices.size();
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
ibd.MiscFlags = 0;
ibd.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA iinitData;
iinitData.pSysMem = &indices[0];
HR(device->CreateBuffer(&ibd, &iinitData, &mQuadPatchIB));
}

19.2.2 地形頂點着色器

因爲我們正在使用曲面細分,所以頂點着色器按照每個控制點進行操作。 我們的頂點着色器幾乎是一個簡單的傳遞着色器,不同之處在於我們通過讀取高度貼圖值來爲貼片控制點進行位移貼圖。 這使控制點的ycoordinates在適當的高度。 這樣做的原因是,在外殼着色器中,我們將計算每個補丁和眼睛之間的距離; 使色塊角偏移到適當的高度使得該距離計算比在xz平面中具有色塊更精確。

Texture2D gHeightMap;
SamplerState samHeightmap
{
Filter = MIN_MAG_LINEAR_MIP_POINT;
AddressU = CLAMP;
AddressV = CLAMP;
};
struct VertexIn
{
float3 PosL : POSITION;
float2 Tex : TEXCOORD0;
float2 BoundsY : TEXCOORD1;
};
struct VertexOut
{
float3 PosW : POSITION;
float2 Tex : TEXCOORD0;
float2 BoundsY : TEXCOORD1;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Terrain specified directly in world space.
vout.PosW = vin.PosL;
// Displace the patch corners to world space. This is to make
// the eye to patch distance calculation more accurate.
vout.PosW.y = gHeightMap.SampleLevel(samHeightmap, vin.Tex, 0).r;
// Output vertex attributes to next stage.
vout.Tex = vin.Tex;
vout.BoundsY = vin.BoundsY;
return vout;
}

19.2.3 鑲嵌因子

外殼着色器常量函數負責計算每個修補程序的曲面細分因子,以指示細分每個修補程序的程度。 另外,我們可以使用外殼着色器常量函數在GPU上進行平截頭體剔除。 我們將在第19.3節中解釋基於GPU的補丁截取。

我們計算眼睛位置和每個色塊邊緣的中點之間的距離,以導出邊緣細分因子。對於內部曲面細分因子,我們計算眼睛位置和色塊中點之間的距離。 我們使用以下代碼從距離中導出曲面細分因子:

// When distance is minimum, the tessellation is maximum.
// When distance is maximum, the tessellation is minimum.
float gMinDist;
float gMaxDist;
// Exponents for power of 2 tessellation. The tessellation
// range is [2^(gMinTess), 2^(gMaxTess)]. Since the maximum
// tessellation is 64, this means gMaxTess can be at most 6
// since 2^6 = 64, and gMinTess must be at least 0 since 2^0 = 1.
float gMinTess;
float gMaxTess;
float CalcTessFactor(float3 p)
{
float d = distance(p, gEyePosW);
float s = saturate((d - gMinDist) / (gMaxDist - gMinDist));
return pow(2, (lerp(gMaxTess, gMinTess, s)));
}

請注意,這個公式不同於第18章中做位移映射時使用的公式。我們使用2的冪,因爲這意味着在每個細節的細節上,細分的數量加倍。例如,假設我們的細節級別爲23=8 ,下一個細化將細分數量加倍爲24=16 ,細節級別爲細分數量的一半22=4 。使用2的冪數 功能可以隨距離更好地展現細節層次。

現在,在常量外殼着色器函數中,我們將此函數應用於貼片中點,並且修補邊緣中點計算曲面細分因子:

struct PatchTess
{
float EdgeTess[4] : SV_TessFactor;
float InsideTess[2] : SV_InsideTessFactor;
};
PatchTess ConstantHS(InputPatch<VertexOut, 4> patch,
uint patchID : SV_PrimitiveID)
{
PatchTess pt;
//
// Frustum cull
//
[... Omit frustum culling code]
//
// Do normal tessellation based on distance.
//
else
{
// It is important to do the tess factor calculation
// based on the edge properties so that edges shared
// by more than one patch will have the same
// tessellation factor. Otherwise, gaps can appear.
// Compute midpoint on edges, and patch center
float3 e0 = 0.5f*(patch[0].PosW + patch[2].PosW);
float3 e1 = 0.5f*(patch[0].PosW + patch[1].PosW);
float3 e2 = 0.5f*(patch[1].PosW + patch[3].PosW);
float3 e3 = 0.5f*(patch[2].PosW + patch[3].PosW);
float3 c = 0.25f*(patch[0].PosW + patch[1].PosW +
patch[2].PosW + patch[3].PosW);
pt.EdgeTess[0] = CalcTessFactor(e0);
pt.EdgeTess[1] = CalcTessFactor(e1);
pt.EdgeTess[2] = CalcTessFactor(e2);
pt.EdgeTess[3] = CalcTessFactor(e3);
pt.InsideTess[0] = CalcTessFactor(c);
pt.InsideTess[1] = pt.InsideTess[0];
return pt;
}
}

19.2.4位移映射

回想一下,域着色器就像鑲嵌細分的頂點着色器。 爲每個生成的頂點評估域着色器。 我們在域着色器中的任務是使用曲面細分頂點位置的參數(u,v)座標來插入控制點數據以導出實際的頂點位置和紋理座標。 另外,我們對高度圖進行採樣以執行置換映射。

{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float2 Tex : TEXCOORD0;
float2 TiledTex : TEXCOORD1;
};
// How much to tile the texture layers.
float2 gTexScale = 50.0f;
[domain("quad")]
DomainOut DS(PatchTess patchTess,
float2 uv : SV_DomainLocation,
const OutputPatch<HullOut, 4> quad)
{
DomainOut dout;
// Bilinear interpolation.
dout.PosW = lerp(
lerp(quad[0].PosW, quad[1].PosW, uv.x),
lerp(quad[2].PosW, quad[3].PosW, uv.x),
uv.y);
dout.Tex = lerp(
lerp(quad[0].Tex, quad[1].Tex, uv.x),
lerp(quad[2].Tex, quad[3].Tex, uv.x),
uv.y);
// Tile layer textures over terrain.
dout.TiledTex = dout.Tex*gTexScale;
// Displacement mapping
dout.PosW.y = gHeightMap.SampleLevel(samHeightmap, dout.Tex, 0).r;
// NOTE: We tried computing the normal in the domain shader
// using finite difference, but the vertices move continuously
// with fractional_even which creates noticable light shimmering
// artifacts as the normal changes. Therefore, we moved the
// calculation to the pixel shader.
// Project to homogeneous clip space.
dout.PosH = mul(float4(dout.PosW, 1.0f), gViewProj);
return dout;
}


圖19.9 中央差異。 差異=h(x+h)h(xh)2h 給出了x方向上切線向量的斜率。 我們使用這個差值作爲點x處切線向量的估計值。

19.2.5 正切和正態矢量估計

我們使用中心差異估計像素着色器中高度貼圖上的切線向量(一個在+ x方向,另一個在-z方向)(參見圖19.9):

Tx(x,z)=(1,hx,0)(1,h(x+h,z)h(xh,z)2h,0)Tz(x,z)=(0,hz,1)(0,h(x,zh)h(x,z+h)2h,1)

我們取負z方向,因爲該方向對應於紋理空間v軸; 所以這些矢量也有助於形成法線貼圖的切線空間。 一旦我們估計了正x方向和負z方向的切向量,我們就可以通過交叉乘積來計算法向量:
// Spacing between height values in normalized uv-space [0,1].
float gTexelCellSpaceU;
float gTexelCellSpaceV;
// Spacing between cells in world space.
float gWorldCellSpace;
//
// Estimate normal and tangent using central differences.
//
float2 leftTex = pin.Tex + float2(-gTexelCellSpaceU, 0.0f);
float2 rightTex = pin.Tex + float2(gTexelCellSpaceU, 0.0f);
float2 bottomTex = pin.Tex + float2(0.0f, gTexelCellSpaceV);
float2 topTex = pin.Tex + float2(0.0f, -gTexelCellSpaceV);
float leftY = gHeightMap.SampleLevel(samHeightmap, leftTex, 0).r;
float rightY = gHeightMap.SampleLevel(samHeightmap, rightTex, 0).r;
float bottomY = gHeightMap.SampleLevel(samHeightmap, bottomTex, 0).r;
float topY = gHeightMap.SampleLevel(samHeightmap, topTex, 0).r;
float3 tangent = normalize(
float3(2.0f*gWorldCellSpace, rightY - leftY, 0.0f));
float3 bitan = normalize(
float3(0.0f, bottomY - topY, -2.0f*gWorldCellSpace));
float3 normalW = cross(tangent, bitan);

我們嘗試使用中心差分來計算域着色器中的法線,但頂點會以fractional_even曲面細分連續移動,從而產生與正常變化一樣明顯的光線閃爍僞影。 因此,我們將計算移至像素着色器。 不用在着色器中計算法線,也可以從離線高度圖生成法線貼圖; 這會在內存中交換一些GPU計算時間。 如果您決定使用紋理貼圖,則只需存儲h/x||Tx||h/x||Tz|| 即可節省內存。 然後,您可以重構Tx||Tx||Tz||Tz|| 着色器中的法向量。這需要每個元素兩個字節,並且除了法向矢量之外還爲您提供切線向量。

19.3防塵貼片

地形通常覆蓋廣闊的區域,我們的許多補丁不會被相機看到。 這表明平截頭體剔除將是一個很好的優化。 如果修補程序的曲面細分因子全部爲零,則GPU會將該修補程序從進一步處理中丟棄; 這意味着不會浪費工作量,只爲那些三角形鑲嵌一個補丁,然後在裁剪階段被淘汰。

爲了進行平截頭體剔除,我們需要兩個成分:我們需要視錐體平面,並且我們需要每個補丁的邊界體積。 第15章練習2解釋瞭如何提取視錐平面。 實現本練習解決方案的代碼如下(在d3dUtil.h / d3dUtil.cpp中實現):

void ExtractFrustumPlanes(XMFLOAT4 planes[6], CXMMATRIX M)
{
//
// Left
//
planes[0].x = M(0,3) + M(0,0);
planes[0].y = M(1,3) + M(1,0);
planes[0].z = M(2,3) + M(2,0);
planes[0].w = M(3,3) + M(3,0);
//
// Right
//
planes[1].x = M(0,3) - M(0,0);
planes[1].y = M(1,3) - M(1,0);
planes[1].z = M(2,3) - M(2,0);
planes[1].w = M(3,3) - M(3,0);
//
// Bottom
//
planes[2].x = M(0,3) + M(0,1);
planes[2].y = M(1,3) + M(1,1);
planes[2].z = M(2,3) + M(2,1);
planes[2].w = M(3,3) + M(3,1);
//
// Top
//
planes[3].x = M(0,3) - M(0,1);
planes[3].y = M(1,3) - M(1,1);
planes[3].z = M(2,3) - M(2,1);
planes[3].w = M(3,3) - M(3,1);
//
// Near
//
planes[4].x = M(0,2);
planes[4].y = M(1,2);
planes[4].z = M(2,2);
planes[4].w = M(3,2);
//
// Far
//
planes[5].x = M(0,3) - M(0,2);
planes[5].y = M(1,3) - M(1,2);
planes[5].z = M(2,3) - M(2,2);
planes[5].w = M(3,3) - M(3,2);
// Normalize the plane equations.
for(int i = 0; i < 6; ++i)
{
XMVECTOR v = XMPlaneNormalize(XMLoadFloat4(&planes[i]));
XMStoreFloat4(&planes[i], v);
}
}

接下來,我們需要一個關於每個補丁的邊界卷。 因爲每個補丁都是矩形的,我們選擇一個軸對齊的邊界框作爲邊界體積。 因爲我們在xz平面中將補丁構造爲矩形,所以補丁控制點固有地編碼x和z座標邊界。 那麼y座標邊界呢? 爲了獲得y座標邊界,我們必須做一個預處理步驟。 每個補丁包含65×65個高度圖元素。 因此,對於每個補丁,我們掃描補丁覆蓋的高度圖條目,並計算最小和最大y座標。 然後我們將這些值存儲在補丁的左上角控制點中,以便我們可以訪問常量外殼着色器中補丁的y邊界。 以下代碼顯示我們計算每個補丁的y邊界:

// x-stores minY, y-stores maxY.
std::vector<XMFLOAT2> mPatchBoundsY;
void Terrain::CalcAllPatchBoundsY()
{
mPatchBoundsY.resize(mNumPatchQuadFaces);
// For each patch
for(UINT i = 0; i < mNumPatchVertRows-1; ++i)
{
for(UINT j = 0; j < mNumPatchVertCols-1; ++j)
{
CalcPatchBoundsY(i, j);
}
}
}
void Terrain::CalcPatchBoundsY(UINT i, UINT j)
{
// Scan the heightmap values this patch covers and
// compute the min/max height.
UINT x0 = j*CellsPerPatch;
UINT x1 = (j+1)*CellsPerPatch;
UINT y0 = i*CellsPerPatch;
UINT y1 = (i+1)*CellsPerPatch;
float minY = +MathHelper::Infinity;
float maxY = -MathHelper::Infinity;
for(UINT y = y0; y <= y1; ++y)
{
for(UINT x = x0; x <= x1; ++x)
{
UINT k = y*mInfo.HeightmapWidth + x;
minY = MathHelper::Min(minY, mHeightmap[k]);
maxY = MathHelper::Max(maxY, mHeightmap[k]);
}
}
UINT patchID = i*(mNumPatchVertCols-1)+j;
mPatchBoundsY[patchID] = XMFLOAT2(minY, maxY);
}
void Terrain::BuildQuadPatchVB(ID3D11Device* device)
{
[...]
// Store axis-aligned bounding box y-bounds in upper-left patch corner.
for(UINT i = 0; i < mNumPatchVertRows-1; ++i)
{
for(UINT j = 0; j < mNumPatchVertCols-1; ++j)
{
UINT patchID = i*(mNumPatchVertCols-1)+j;
patchVertices[i*mNumPatchVertCols+j].BoundsY = mPatchBoundsY[patchID];
}
}[
...]
}

現在在恆定的外殼着色器中,我們可以構造我們的軸對齊的邊界框,並執行框/平截頭體相交測試,以查看框位於平截頭體之外。 我們使用與第15章中介紹的不同的盒子/平面相交測試。它實際上是第15章練習4中描述的OBB/plane 測試的一個特例。因爲盒子是軸對齊的,因此半徑r簡化:

a0r0=(a0,0,0)a1r1=(0,a1,0)a2r2=(0,0,a2)r=|a0r0·n|+|a1r1·n|+|a2r2·n|=a0|nx|+a1|ny|+a2|nz|

我們喜歡對第15.2.3.3節中的測試,因爲它不包含條件語句。
float4 gWorldFrustumPlanes[6];
// Returns true if the box is completely behind (in negative
// half space) of plane.
bool AabbBehindPlaneTest(float3 center, float3 extents, float4 plane)
{
float3 n = abs(plane.xyz); // (|n.x|, |n.y|, |n.z|)
// This is always positive.
float r = dot(extents, n);
// signed distance from center point to plane.
float s = dot(float4(center, 1.0f), plane);
// If the center point of the box is a distance of e or more behind the
// plane (in which case s is negative since it is behind the plane),
// then the box is completely in the negative half space of the plane.
return (s + e) < 0.0f;
}/
/ Returns true if the box is completely outside the frustum.
bool AabbOutsideFrustumTest(float3 center, float3 extents, float4 frustumPlanes[6])
{
for(int i = 0; i < 6; ++i)
{
// If the box is completely behind any of the frustum planes
// then it is outside the frustum.
if(AabbBehindPlaneTest(center, extents, frustumPlanes[i]))
{
return true;
}
}
return false;
}
PatchTess ConstantHS(InputPatch<VertexOut, 4> patch, uint patchID : SV_PrimitiveID)
{
PatchTess pt;
//
// Frustum cull
//
// We store the patch BoundsY in the first control point.
float minY = patch[0].BoundsY.x;
float maxY = patch[0].BoundsY.y;
// Build axis-aligned bounding box. patch[2] is lower-left corner
// and patch[1] is upper-right corner.
float3 vMin = float3(patch[2].PosW.x, minY, patch[2].PosW.z);
float3 vMax = float3(patch[1].PosW.x, maxY, patch[1].PosW.z);
// Center/extents representation.
float3 boxCenter = 0.5f*(vMin + vMax);
float3 boxExtents = 0.5f*(vMax - vMin);
if(AabbOutsideFrustumTest(boxCenter, boxExtents, gWorldFrustumPlanes))
{
pt.EdgeTess[0] = 0.0f;
pt.EdgeTess[1] = 0.0f;
pt.EdgeTess[2] = 0.0f;
pt.EdgeTess[3] = 0.0f;
pt.InsideTess[0] = 0.0f;
pt.InsideTess[1] = 0.0f;
return pt;
}/
/
// Do normal tessellation based on distance.
//
else
{
[...]
}


圖19.10 OBB /plane 相交測試。我們可以對AABB使用相同的測試,因爲AABB是OBB的特例。此外,r的公式簡化了AABB的情況。

19.4紋理

回想一下§8.10,我們在山上鋪平了草地的紋理。我們平鋪紋理以提高分辨率(即,增加覆蓋地塊上三角形的紋理樣本數量)。我們想在這裏做同樣的事情;然而,我們不希望被限制爲單一的草地紋理。我們想要同時創建描繪沙子,草地,泥土,岩石和雪的地形。您可能會建議創建一個包含沙子,草地,泥土等的大質地,並將其在地形上展開。但是這會導致我們回到分辨率問題 - 地形幾何非常大,我們需要一個不切實際的大紋理來獲得足夠的顏色樣本來獲得體面的分辨率。相反,我們採用了像素透明混合那樣的多紋理方法。

這個想法是爲每個地形圖層都有一個單獨的紋理(例如,一個用於草地,泥土,岩石等)。這些紋理將平鋪在高分辨率的地形上。舉例來說,假設我們有3個地形層(草地,泥土和岩石);然後將這些圖層合併起來,如圖19.11所示。

以前的過程應該讓人想起透明度alpha混合。存儲我們正在編寫的圖層的源Alpha的混合貼圖指示源圖層的不透明度,從而允許我們控制多少源圖層覆蓋現有的地形顏色。這使得我們可以用草地塗抹地形的某些部分,用灰塵塗抹一些部分,用雪塗抹其他部分,或者使用這三種混合物。


圖19.11。 (頂部)首先放置第0層(草)作爲當前的地形顏色。 (中)現在通過透明度alpha混合公式將當前地形顏色與第一層(污垢)混合; 混合貼圖提供源alpha分量。 (底部)最後,通過透明度alpha混合公式將當前地形顏色與第二層(岩石)混合; 混合貼圖提供源alpha分量。

圖19.11說明了具有3個顏色映射的過程。在我們的代碼中,我們使用5.爲了組合5個彩色地圖,我們需要4個灰度混合地圖。 我們可以將這4個灰度混合貼圖打包成單個RGBA貼圖(紅色通道存儲第一個混合貼圖,綠色通道存儲第二個混合貼圖,藍色通道存儲第三個混合貼圖,Alpha通道存儲第四個混合貼圖)。 因此,需要總共六個紋理來實現這一點。以下地形像素着色器代碼顯示了我們的紋理混合是如何實現的:

// Sample layers in texture array.
float4 c0 = gLayerMapArray.Sample(samLinear, float3(pin.TiledTex, 0.0f));
float4 c1 = gLayerMapArray.Sample(samLinear, float3(pin.TiledTex, 1.0f));
float4 c2 = gLayerMapArray.Sample(samLinear, float3(pin.TiledTex, 2.0f));
float4 c3 = gLayerMapArray.Sample(samLinear, float3(pin.TiledTex, 3.0f));
float4 c4 = gLayerMapArray.Sample(samLinear, float3(pin.TiledTex, 4.0f));
// Sample the blend map.
float4 t = gBlendMap.Sample(samLinear, pin.Tex);
// Blend the layers on top of each other.
float4 texColor = c0;
texColor = lerp(texColor, c1, t.r);
texColor = lerp(texColor, c2, t.g);
texColor = lerp(texColor, c3, t.b);
texColor = lerp(texColor, c4, t.a);

混合貼圖通常取決於地形的高度貼圖。因此,我們不會將混合貼圖嵌入到彩色貼圖的Alpha通道中。如果我們這樣做了,那麼顏色貼圖只適用於特定的高度貼圖。通過分開保存顏色貼圖和混合貼圖,我們可以重複使用許多不同地形的顏色貼圖。另一方面,將爲每個地形構建不同的混合地圖。

與圖層紋理不同,混合貼圖不會平鋪,因爲我們將它們拉伸到整個地形表面。這是必要的,因爲我們使用混合貼圖來標記要顯示特定紋理的地形區域,所以混合貼圖必須是全局的並覆蓋整個地形。您可能想知道這是可以接受的還是過度放大。事實上,放大會發生,並且混合貼圖在整個地形上被拉伸時會被紋理過濾扭曲,但混合貼圖不是我們從細節中獲取細節的地方(我們從平鋪紋理中獲取細節)。混合貼圖僅僅標記特定紋理貢獻的地形的一般區域(以及多少)。因此,如果混合地圖變形並模糊,它不會對最終結果產生顯着影響 - 例如,可能會有一些灰塵與草地混合在一起,而這實際上提供了層之間更平滑的過渡。

NOTE:您還可以通過在地形上平鋪法線貼圖來添加法線貼圖支持。 像岩石這樣的材料可以從法線貼圖提供的更高細節中受益。 正如我們在§19.3中看到的,我們確實計算每個像素的切線幀。

19.5地形高度

一個常見的任務是獲得給定x座標和z座標的地形表面的高度。這對於將物體放置在地形表面上,或者將攝像機輕輕放在地形表面上以模擬玩家在地形上行走時非常有用。

高度圖給出了網格點處地形頂點的高度。但是,我們需要頂點之間的地形高度。因此,給定離散高度圖採樣,我們必須進行插值以形成表示地形的連續曲面y = h(x,z)。由於地形近似爲三角形網格,因此使用線性插值可以使我們的高度函數與底層地形網格幾何體一致。

要開始解決這個問題,我們的第一個目標是找出x座標和z座標位於哪個單元格中。(注意:我們假設座標x和z與地形的局部空間有關)。這個;它告訴我們x和z座標所在的單元格的行和列。

// Transform from terrain local space to "cell" space.
float c = (x + 0.5f*width()) / mInfo.CellSpacing;
float d = (z - 0.5f*depth()) / -mInfo.CellSpacing;
// Get the row and column we are in.
int row = (int)floorf(d);
int col = (int)floorf(c);

圖19.12ab解釋了這段代碼的作用。 本質上,我們正在轉換到一個新的座標系,其中原點位於最左上角的地形頂點,正z軸下移,並且每個單位都縮放到該座標系,直到它對應於一個單元格空間。現在,在此座標系 系統,圖19.12b清楚地表明瞭單元的行和列分別由floor(z)和floor(x)給出。 在圖例中,該點位於第4行和第1列(使用基於零的索引)。 (回想floor(t)的值是小於或等於t的最大整數。)還要注意row和col給出單元格左上頂點的索引。

現在我們知道我們所在的單元格了,我們從高度圖中獲取四個單元頂點的高度:

// Grab the heights of the cell we are in.
// A*--*B
// | /|
// |/ |
// C*--*D
float A = mHeightmap[row*mInfo.HeightmapWidth + col];
float B = mHeightmap[row*mInfo.HeightmapWidth + col + 1];
float C = mHeightmap[(row+1)*mInfo.HeightmapWidth + col];
float D = mHeightmap[(row+1)*mInfo.HeightmapWidth + col + 1];


圖19.12。 (a)相對於地形座標系的xz平面中的點具有座標(x,z)。 (b)我們選擇一個新的座標系統,其中原點是最左上方的網格頂點,正的z軸下移,並且每個單位都被縮放,以便它對應於一個單元格空間。 該點具有相對於該座標系的座標(c,d)。 這種轉變涉及到翻譯和縮放。 一旦在這個新的座標系中,找到我們所在的單元格的行和列是微不足道的。 (c)我們引入第三個座標系,它的起點位於點所在單元的左上頂點。該點具有相對於該座標系的座標(s,t)。 將座標轉換爲該系統只涉及簡單的平移以抵消座標。 觀察如果s + t≤1,我們處於“上”三角形,否則我們處於“下”三角形。

在這一點上,我們知道我們所在的單元格,並且我們知道該單元格的四個頂點的高度。 現在我們需要在特定的x座標和z座標處找到地形曲面的高度(y座標)。 這有點棘手,因爲細胞可以傾向於幾個方向; 見圖19.13。

圖19.13 地形表面在特定x座標和z座標處的高度(y座標)。

爲了找到高度,我們需要知道我們所處細胞的哪個三角形(回想起我們的細胞呈現爲兩個三角形)。 爲了找到高度,我們需要知道我們所在的細胞的三角形(回想起我們的細胞呈現爲兩個三角形)。 要找到我們所在的三角形,我們要改變我們的座標,以便相對於單元座標系來描述座標(c,d)(見圖19.12c)。 這種簡單的座標變換隻涉及翻譯,並按如下方式完成:

float s = c - (float)col;
float t = d - (float)row;

那麼,如果s + t≤1,我們處於“上”三角形ΔABC中,否則我們處於“下”三角形ΔDCB中。

現在我們解釋如何在“上”三角形中找到高度。 該過程與“較低”三角形相似,當然,兩者的代碼很快就會出現。 爲了找到高度,如果我們處於“上”三角形,我們首先在三角形的邊上構造兩個向量u =(Δx,B-A,0)和v =(0,C - A, - Δz) 從終點Q開始,如圖19.14a所示。 然後我們用s對u進行線性插值,然後沿着v對t進行線性插值。 圖19.14b說明了這些插值。 Q + su + tv點的y座標給出了基於給定的x座標和z座標的高度(回想一下向量加法的幾何解釋來看這個)。

請注意,因爲我們只關心插值高度值,我們可以插入y分量並忽略其他分量。 因此,高度由總和A+suy+tvy 獲得。

因此,Terrain :: GetHeight代碼的結論是:


圖19.14 (b)在上三角形邊緣計算兩個向量。(b)高度是矢量的y座標。

// If upper triangle ABC.
if(s + t <= 1.0f)
{
float uy = B - A;
float vy = C - A;
return A + s*uy + t*vy;
}
else // lower triangle DCB.
{
float uy = C - D;
float vy = B - D;
return D + (1.0f-s)*uy + (1.0f-t)*vy;
}
We can now clamp the camera above the terrain to simulate that the player is walking on the terrain:
void TerrainApp::UpdateScene(float dt)
{
//
// Control the camera.
//
if(GetAsyncKeyState('W') & 0x8000)
mCam.Walk(10.0f*dt);
if(GetAsyncKeyState('S') & 0x8000)
mCam.Walk(-10.0f*dt);
if(GetAsyncKeyState('A') & 0x8000)
mCam.Strafe(-10.0f*dt);
if(GetAsyncKeyState('D') & 0x8000)
mCam.Strafe(10.0f*dt);
//
// Walk/fly mode
//
if(GetAsyncKeyState('2') & 0x8000)
mWalkCamMode = true;
if(GetAsyncKeyState('3') & 0x8000)
mWalkCamMode = false;
//
// Clamp camera to terrain surface in walk mode.
//
if(mWalkCamMode)
{
XMFLOAT3 camPos = mCam.GetPosition();
float y = mTerrain.GetHeight(camPos.x, camPos.z);
mCam.SetPosition(camPos.x, y + 2.0f, camPos.z);
}
mCam.UpdateViewMatrix();
}

19.6總結

1.我們可以使用三角形網格對地形進行建模,其中每個頂點的高度均以模擬山丘和山谷的方式指定。
2.高度圖是一個矩陣,其中每個元素指定地形網格中特定頂點的高度。在每個網格頂點的高度圖中存在一個條目,並且第ij個高度圖條目提供第i個頂點的高度。高度圖通常在視覺上被表示爲灰度圖,其中黑色表示最小高度,白色表示最大高度,以及
灰色代表兩者之間的高度。
3.地形覆蓋大面積,因此構建它們所需的三角形數量很大。如果我們使用均勻鑲嵌網格,則由於透視投影的性質,地形的屏幕空間三角形密度將隨距離增加。這實際上與我們想要的相反:我們希望三角形密度在照相機附近很大,細節將被注意到,並且三角形密度距離相機較遠的地方更小,其中細節將被忽略。我們可以使用硬件細分來根據距相機的距離來實現連續的細節水平。地形曲面細分的總體策略可以總結如下:
a)放下四個補丁網格。
b)根據它們距相機的距離對貼片進行細分。
c)將高度圖綁定爲着色器資源。在域着色器中,從高度圖執行位移映射以將生成的頂點偏移到其正確的高度。
4.我們可以在GPU上實施平截頭體剔除,以在平截頭體之外剔除四塊補丁。這是在常量外殼着色器函數中完成的,通過將所有曲面細分因數設置爲零來獲得平截頭以外的補丁。
5.我們通過在彼此之間混合層來構造地形(例如,草地,泥土,岩石,雪)。混合貼圖用於控制每個圖層對最終地形圖像的貢獻量。
6.高度圖給我們在網格點處的地形頂點的高度,但是我們還需要頂點之間的地形高度。因此,給定離散高度圖採樣,我們必須進行插值以形成表示地形的連續曲面y = h(x,z)。由於地形近似爲三角形網格,因此使用線性插值可以使我們的高度函數與底層地形網格幾何體一致。具有地形的高度功能對於將物體放置在地形表面上或將相機稍微放置在地形表面上以模擬玩家在地形上行走時很有用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章