Windows 八 Directx 开发学习笔记(七)水波纹的实现
使用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里都可以使用。为了达到更真实的效果,还可以考虑石子的大小,入水时的力度等等。