使用DirectX實際開發中,模型的形狀不可能都是一成不變,只依靠移動攝像機去實現動畫。這裏用實時更新頂點緩衝的方式生成一個水波模型,最終效果類似向水面扔石子時出現的水波紋。有了上一篇建立好的模型,實現這個效果僅需要更改WaterModel類和Renderer類裏m_water的相關調用。
首先更改WaterModel的頭文件,添加模擬變量成員和臨時緩衝區指針,並增加兩個方法Disturb和Update,整個類代碼如下:
class WaterModel
{
public:
WaterModel(void);
~WaterModel(void);
void Initialize(ID3D11Device* d3dDevice, int m, int n, float dx, float dt, float speed, float damping);
void Render(ID3D11DeviceContext* d3dContext);
void Disturb(int i, int j, float magnitude);
void Update(ID3D11DeviceContext* d3dContext, float dt);
private:
Microsoft::WRL::ComPtr<ID3D11Buffer> m_vertexBuffer;
Microsoft::WRL::ComPtr<ID3D11Buffer> m_indexBuffer;
uint32 m_vertexCount,m_indexCount;
//二維網格的行數、列數
int xRange;
int zRange;
// 欲計算的模擬常量.
float mK1;
float mK2;
float mK3;
float mTimeStep;
float mSpatialStep;
//頂點臨時緩衝
XMFLOAT3* mPrevSolution;
XMFLOAT3* mCurrSolution;
};
完成後就開始更改WaterModel的構造方法,使用初始化列表初始化成員:
WaterModel::WaterModel(void):
m_indexCount(0),m_vertexCount(0),
mK1(0.0f),mK2(0.0f), mK3(0.0f),
mTimeStep(0.0f),mSpatialStep(0.0f),
mPrevSolution(0),mCurrSolution(0),
xRange(128),zRange(128)
{
}
然後是更改Initialize方法。其中模擬常量等與算法有關的部分按照DirectX 10的例子進行計算,代碼如下:
xRange = m;
zRange = n;
m_vertexCount = m*n;
m_indexCount = (m-1)*(n-1)*2*3;
mTimeStep = dt;
mSpatialStep = dx;
float d = damping*dt+2.0f;
float e = (speed*speed)*(dt*dt)/(dx*dx);
mK1 = (damping*dt-2.0f)/ d;
mK2 =(4.0f-8.0f*e) / d;
mK3 = (2.0f*e) / d;
接着要爲臨時緩衝區分配空間並初始化,假設靜止時水面的y座標均爲零。
mPrevSolution = new XMFLOAT3[m*n];
mCurrSolution = new XMFLOAT3[m*n];
//生成水平面
for(int row=0;row<xRange; ++row)
{
float zPos = row*dx;
for(int col=0;col<zRange; ++col)
{
float xPos = col*dx;
mPrevSolution[xRange*row+ col] = XMFLOAT3(xPos, 0.0f, zPos);
mCurrSolution[xRange*row+ col] = XMFLOAT3(xPos, 0.0f, zPos);
}
}
爲了讓頂點緩衝區能夠動態更新,需要將Usage和CPUAccessFlags兩個標誌分別設置爲D3D11_USAGE_DYNAMIC和D3D11_CPU_ACCESS_WRITE。另外,頂點緩衝區會在Update方法裏填充,所以將CreateBuffer方法中代表數據源地址的第二個參數設爲0。
D3D11_BUFFER_DESC vertexBufferDesc;
vertexBufferDesc.Usage = D3D11_USAGE_DYNAMIC;
vertexBufferDesc.ByteWidth = sizeof(VertexPositionColor) * m_vertexCount;
vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vertexBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
vertexBufferDesc.MiscFlags = 0;
vertexBufferDesc.StructureByteStride = 0;
DX::ThrowIfFailed(
d3dDevice->CreateBuffer(
&vertexBufferDesc,
0,
&m_vertexBuffer
)
);
xRange與zRange現在不是常量,需要使用new的方式新建數組。
unsigned short* Indices = new unsigned short[3*2*(xRange-1)*(zRange-1)];
Indices現在是一個指針,不能用sizeof(Indices)來獲得索引數組的大小,所以構造索引緩衝區的代碼改爲:
CD3D11_BUFFER_DESC indexBufferDesc(sizeof(unsigned short) * m_indexCount, D3D11_BIND_INDEX_BUFFER);
在Initialize方法最後,刪除Indices指向的索引數組,完成Initialize方法的更改。
delete[] Indices;
新成員方法Disturb的功能是在水面上隨機選取一個點,提高它的高度,模擬水花濺起的一瞬間。爲了保證曲面平滑,最高點周圍四點的高度爲幅值一半。
void WaterModel::Disturb(int i, int j, float magnitude)
{
// 跳過邊界點
assert(i > 1 && i < xRange-2);
assert(j > 1 && j < zRange-2);
float halfMag = 0.5f*magnitude;
// 首先改變ij的高度,然後改變ij周圍點的高度
mCurrSolution[i*xRange+j].y += magnitude;
mCurrSolution[i*xRange+j+1].y += halfMag;
mCurrSolution[i*xRange+j-1].y += halfMag;
mCurrSolution[(i+1)*xRange+j].y += halfMag;
mCurrSolution[(i-1)*xRange+j].y += halfMag;
}
新成員方法Update的功能主要是模擬水波隨時間傳播的過程。要使用兩個數組存儲水面頂點座標是因爲計算當前幀的水面座標時需要考慮前一幀的座標。方法中還有很重要的一部分就是更新頂點緩衝區。爲保證安全更新緩衝區,使用Map方法將其鎖定,更新完成後再用Unmap解鎖。整個方法的詳細代碼如下:
void WaterModel::Update(ID3D11DeviceContext* d3dContext, float dt)
{
static float t = 0;
// 統計時間
t += dt;
// 達到一定時間後進行更新
if( t >= mTimeStep)
{
// 只更新邊界內的點,所以循環範圍爲1--(range-1)
for(int row=1;row<(xRange-1); ++row)
{
for(int col=1;col<(zRange-1); ++col)
{
// 更新之後要交換緩衝區,所以不需要保存mPrevSolution的數據
// 直接計算完成後覆蓋即可
mPrevSolution[row*xRange+col].y=
mK1*mPrevSolution[row*xRange+col].y+
mK2*mCurrSolution[row*xRange+col].y+
mK3*(mCurrSolution[(row+1)*xRange+col].y+
mCurrSolution[(row-1)*xRange+col].y+
mCurrSolution[row*xRange+col+1].y+
mCurrSolution[row*xRange+col-1].y);
}
}
// 新舊緩衝區交換,類似交換鏈
std::swap(mPrevSolution,mCurrSolution);
t = 0.0f; // 重新統計時間
// 更新頂點緩衝區
D3D11_MAPPED_SUBRESOURCE mappedResource;
VertexPositionColor* vertex;
DX::ThrowIfFailed(
d3dContext->Map(
m_vertexBuffer.Get(),
0,
D3D11_MAP_WRITE_DISCARD,
0,
&mappedResource)
);
// 得到一個頂點緩衝的指針
vertex = (VertexPositionColor*)mappedResource.pData;
// 生成新的頂點數據
const XMFLOAT3 BLUE(0.0f, 0.0f,1.0f);
for(uint32 i = 0; i <m_vertexCount; ++i)
{
vertex[i].pos= mCurrSolution[i];
vertex[i].color= BLUE;
}
// 解鎖頂點緩衝
d3dContext->Unmap(m_vertexBuffer.Get(),0);
}
}
以上就是WaterModel類中所有需要修改的部分。工具已經配置好,就看如何使用了。很自然從修改Renderer類的Initialize方法開始,因爲WaterModel的Initialize方法的定義已經改變,設置模擬常量,用以下代碼實現調用:
auto createWaterTask = (createPSTask &&createVSTask).then([this] () {
m_water.Initialize(m_d3dDevice.Get(),128, 128, 1.0f, 0.03f, 3.25f, 0.4f);
});
然後需要修改的就是Renderer類的Update方法。爲了觀察水波效果,需要讓攝像機靜止在一個位置,所以令flypos爲固定值15.0f。更重要的是添加下面的代碼來實現水波紋的模擬:
// 加載完成後更新(加載爲異步過程)。
if (m_loadingComplete)
{
static float t_base = 0.0f;
if( timeTotal - t_base >= 0.5f )
{
t_base += 0.5f;
// 由於xRange=zRange=128,所以i、j必須小於128
int i = 5 + rand() % 120;
int j = 5 + rand() % 120;
// 得到1到2之間的一個浮點數
float r = 1.0 + (float)(rand()) / (float)RAND_MAX*(2.0 - 1.0);
// 更改某點的高度,類似向水裏扔一塊石頭
m_water.Disturb(i, j, r);
}
m_water.Update(m_d3dContext.Get(), timeDelta);
}
儲存t_base的原因是需要通過它來控制扔石子的間隔時間,這裏設置爲0.5秒。每一刻都要計算水波的傳播狀態,所以在條件判斷的外面調用Update方法更新水面頂點緩衝區。編譯運行後的效果如下圖:
從整個過程來看,最重要的是模擬水面波動的算法,其他部分只是調用API,完成固定的渲染流程而已。因此只要完成算法,不管在哪個版本的DirectX裏都可以使用。爲了達到更真實的效果,還可以考慮石子的大小,入水時的力度等等。